Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions docs/2025/puzzles/day08.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,123 @@ import Solver from "../../../../../website/src/components/Solver.js"

# Day 8: Playground

by [@mbovel](https://github.com/mbovel)

## Puzzle description

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

## Data Model

To solve this puzzle, we use a class to represent junction boxes:

```scala
/** A junction box in 3D space with an associated circuit ID. */
class Box(val x: Long, val y: Long, val z: Long, var circuit: Int):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it look more natural / beginner-friendly as

case class Box(x: Long, y: Long, z: Long, var circuit: Int)

I think it's fine either way, just a thought.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question is: do we want two boxes that have the same coordinates and same circuit to be ==? I am not sure.

Also, I thought it would impact .pairs, but .combinations actually doesn't use ==. Whereas we use case class or class, we have:

val b1 = Box(1, 2, 3, 0)
val b2 = Box(1, 2, 3, 0)
assert(Seq(b1, b1).pairs.size == 1)
assert(Seq(b1, b2).pairs.size == 1)

So, I am fine with a case class.

def distanceSquare(other: Box): Long =
(x - other.x) * (x - other.x) + (y - other.y) * (y - other.y) + (z - other.z) * (z - other.z)
```

Each `Box` has:
- Three coordinates (`x`, `y`, `z`) representing its position in 3D space
- A mutable `circuit` field to track which circuit the box belongs to (each circuit is identified by a distinct integer)
- 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)

## Data Loading

The following functions parse the input into a sequence of boxes and compute all unique pairs sorted by distance:

```scala
/** Parses comma-separated coordinates from the given `line` into a `Box`
* with the given `circuit` ID.
*/
def parseBox(line: String, circuit: Int): Box =
val parts = line.split(",")
Box(parts(0).toLong, parts(1).toLong, parts(2).toLong, circuit)

/** Parses the input, returning a sequence of `Box`es and all unique pairs
* of boxes sorted by distance.
*/
def load(input: String): (Seq[Box], Seq[(Box, Box)]) =
val lines = input.linesIterator.filter(_.nonEmpty)
val boxes = lines.zipWithIndex.map(parseBox).toSeq
val pairsByDistance = boxes.pairs.toSeq.sortBy((b1, b2) => b1.distanceSquare(b2))
(boxes, pairsByDistance)
```

The `pairs` extension method generates all unique pairs from a sequence:

```scala
extension [T](self: Seq[T])
/** Generates all unique pairs (combinations of 2) from the sequence. */
def pairs: Iterator[(T, T)] =
self.combinations(2).map(pair => (pair(0), pair(1)))
```

## Part 1

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.

```scala
def part1(input: String): Int =
val (boxes, pairsByDistance) = load(input)
for (b1, b2) <- pairsByDistance.take(1000) if b1.circuit != b2.circuit do
merge(b1.circuit, b2.circuit, boxes)
val sizes = boxes.groupBy(_.circuit).values.map(_.size).toSeq.sortBy(-_)
sizes.take(3).product
```

The `merge` function updates all boxes in one circuit to belong to another:

```scala
/** Sets all boxes with circuit `c2` to circuit `c1`. */
def merge(c1: Int, c2: Int, boxes: Seq[Box]): Unit =
for b <- boxes if b.circuit == c2 do b.circuit = c1
```


## Part 2

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.

```scala
def part2(input: String): Long =
val (boxes, pairsByDistance) = load(input)
var n = boxes.length
boundary:
for (b1, b2) <- pairsByDistance if b1.circuit != b2.circuit do
merge(b1.circuit, b2.circuit, boxes)
n -= 1
if n <= 1 then
break(b1.x * b2.x)
throw Exception("Should not reach here")
```

[`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.

## Potential Optimizations

On my machine, both parts run in under 2 seconds, which is acceptable for this puzzle. However, there are potential optimizations:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting that this is a well-known problem https://en.wikipedia.org/wiki/Minimum_spanning_tree

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment! I refined my "Potential Optimizations" section.


- **Finding the k closest pairs**: Computing all pairs and sorting them has O(n² log n) complexity. A spatial data structure like a [*k*-d tree](https://en.wikipedia.org/wiki/K-d_tree) could find nearest neighbors more efficiently.
- **Merging circuits**: The `merge` function iterates over all boxes to update circuit IDs. A [union-find](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) data structure would track connected components more efficiently.

## Final Code

See the complete code on [GitHub](https://github.com/scalacenter/scala-advent-of-code/blob/main/2025/src/day08.scala).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link is currently missing, I guess the file will be included later on?

Should we just show the whole code here in the article instead? (unless it's too long) Most other articles do that, even though it's a bit repetitive. Again, it's fine either way I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link should work once we merge #912. In general, I prefer to avoid duplicating sources of truth; it’s safer if readers consult the original file directly (which can be CI-tested/linted) rather than a Markdown copy that may drift out of sync. That’s just a general principle though, given that this repo has no CI 😄


## Run it in the browser

Thanks to the [Scala.js](https://www.scala-js.org/) build, you can also experiment with this code directly in the browser.

### Part 1

<Solver puzzle="day08-part1" year="2025"/>

### Part 2

<Solver puzzle="day08-part2" year="2025"/>

## Solutions from the community

Share your solution to the Scala community by editing this page.
Expand Down