-
Notifications
You must be signed in to change notification settings - Fork 90
Add article for day 8 #914
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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): | ||
| 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: | ||
|
||
|
|
||
| - **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). | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
||
There was a problem hiding this comment.
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
I think it's fine either way, just a thought.
There was a problem hiding this comment.
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.combinationsactually doesn't use==. Whereas we usecase classorclass, we have:So, I am fine with a
case class.