Skip to content

Commit 51aa79c

Browse files
authored
Add article for day 8 (#914)
1 parent 7466f81 commit 51aa79c

File tree

1 file changed

+116
-0
lines changed

1 file changed

+116
-0
lines changed

docs/2025/puzzles/day08.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,126 @@ import Solver from "../../../../../website/src/components/Solver.js"
22

33
# Day 8: Playground
44

5+
by [@mbovel](https://github.com/mbovel)
6+
57
## Puzzle description
68

79
https://adventofcode.com/2025/day/8
810

11+
## Data Model
12+
13+
To solve this puzzle, we use a class to represent junction boxes:
14+
15+
```scala
16+
/** A junction box in 3D space with an associated circuit ID. */
17+
case class Box(val x: Long, val y: Long, val z: Long, var circuit: Int):
18+
def distanceSquare(other: Box): Long =
19+
(x - other.x) * (x - other.x) + (y - other.y) * (y - other.y) + (z - other.z) * (z - other.z)
20+
```
21+
22+
Each `Box` has:
23+
- Three coordinates (`x`, `y`, `z`) representing its position in 3D space
24+
- A mutable `circuit` field to track which circuit the box belongs to (each circuit is identified by a distinct integer)
25+
- A `distanceSquare` method that computes the squared Euclidean distance to another box (we use squared distance to avoid computing square roots, since we only need to compare distances)
26+
27+
## Data Loading
28+
29+
The following functions parse the input into a sequence of boxes and compute all unique pairs sorted by distance:
30+
31+
```scala
32+
/** Parses comma-separated coordinates from the given `line` into a `Box`
33+
* with the given `circuit` ID.
34+
*/
35+
def parseBox(line: String, circuit: Int): Box =
36+
val parts = line.split(",")
37+
Box(parts(0).toLong, parts(1).toLong, parts(2).toLong, circuit)
38+
39+
/** Parses the input, returning a sequence of `Box`es and all unique pairs
40+
* of boxes sorted by distance.
41+
*/
42+
def load(input: String): (Seq[Box], Seq[(Box, Box)]) =
43+
val lines = input.linesIterator.filter(_.nonEmpty)
44+
val boxes = lines.zipWithIndex.map(parseBox).toSeq
45+
val pairsByDistance = boxes.pairs.toSeq.sortBy((b1, b2) => b1.distanceSquare(b2))
46+
(boxes, pairsByDistance)
47+
```
48+
49+
The `pairs` extension method generates all unique pairs from a sequence:
50+
51+
```scala
52+
extension [T](self: Seq[T])
53+
/** Generates all unique pairs (combinations of 2) from the sequence. */
54+
def pairs: Iterator[(T, T)] =
55+
self.combinations(2).map(pair => (pair(0), pair(1)))
56+
```
57+
58+
## Part 1
59+
60+
For Part 1, we process the 1000 closest pairs of boxes and merge their circuits. The algorithm iterates through pairs in order of increasing distance; when two boxes belong to different circuits, we merge them into one. Finally, we find the three largest circuits and return the product of their sizes.
61+
62+
```scala
63+
def part1(input: String): Int =
64+
val (boxes, pairsByDistance) = load(input)
65+
for (b1, b2) <- pairsByDistance.take(1000) if b1.circuit != b2.circuit do
66+
merge(b1.circuit, b2.circuit, boxes)
67+
val sizes = boxes.groupBy(_.circuit).values.map(_.size).toSeq.sortBy(-_)
68+
sizes.take(3).product
69+
```
70+
71+
The `merge` function updates all boxes in one circuit to belong to another:
72+
73+
```scala
74+
/** Sets all boxes with circuit `c2` to circuit `c1`. */
75+
def merge(c1: Int, c2: Int, boxes: Seq[Box]): Unit =
76+
for b <- boxes if b.circuit == c2 do b.circuit = c1
77+
```
78+
79+
80+
## Part 2
81+
82+
For Part 2, we continue merging circuits until only one remains. We track the number of distinct circuits and return the product of the x-coordinates of the two boxes in the final merge.
83+
84+
```scala
85+
def part2(input: String): Long =
86+
val (boxes, pairsByDistance) = load(input)
87+
var n = boxes.length
88+
boundary:
89+
for (b1, b2) <- pairsByDistance if b1.circuit != b2.circuit do
90+
merge(b1.circuit, b2.circuit, boxes)
91+
n -= 1
92+
if n <= 1 then
93+
break(b1.x * b2.x)
94+
throw Exception("Should not reach here")
95+
```
96+
97+
[`boundary` and `break`](https://www.scala-lang.org/api/3.x/scala/util/boundary$.html) provide a way to exit the loop early and return a value when only one circuit remains.
98+
99+
## Potential Optimizations
100+
101+
On my machine, both parts run in under two seconds, which is acceptable for this puzzle. Still, several optimizations are possible.
102+
103+
What we implemented is essentially the [Euclidean minimum spanning tree (EMST)](https://en.wikipedia.org/wiki/Euclidean_minimum_spanning_tree) computed via [Kruskal’s algorithm](https://en.wikipedia.org/wiki/Kruskal%27s_algorithm), after generating all pairwise distances.
104+
105+
This algorithm is normally paired with a [union–find](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) structure to maintain connected components efficiently. Using it would speed up our current `merge` step, which is $\mathcal{O}(n)$ per merge, and reduce it to near-constant time.
106+
107+
We could also improve how we find the $k$ closest pairs. Computing all pairs and sorting them has $\mathcal{O}(n^2 \log n)$ complexity. A spatial index such as a [k-d tree](https://en.wikipedia.org/wiki/K-d_tree) would avoid generating all pairs and, in the average case, remove the quadratic blow-up. Another option is to restrict candidates to a geometric graph such as the [relative neighborhood graph](https://en.wikipedia.org/wiki/Relative_neighborhood_graph) or the 3D [Delaunay triangulation](https://en.wikipedia.org/wiki/Delaunay_triangulation), both of which contain the EMST and are much sparser than the complete graph.
108+
109+
## Final Code
110+
111+
See the complete code on [GitHub](https://github.com/scalacenter/scala-advent-of-code/blob/main/2025/src/day08.scala).
112+
113+
## Run it in the browser
114+
115+
Thanks to the [Scala.js](https://www.scala-js.org/) build, you can also experiment with this code directly in the browser.
116+
117+
### Part 1
118+
119+
<Solver puzzle="day08-part1" year="2025"/>
120+
121+
### Part 2
122+
123+
<Solver puzzle="day08-part2" year="2025"/>
124+
9125
## Solutions from the community
10126

11127
- [Solution](https://github.com/merlinorg/advent-of-code/blob/main/src/main/scala/year2025/day08.scala) by [merlinorg](https://github.com/merlinorg)

0 commit comments

Comments
 (0)