|
| 1 | +package day08 |
| 2 | + |
| 3 | +import scala.util.boundary |
| 4 | +import scala.util.boundary.break |
| 5 | + |
| 6 | +import locations.Directory.currentDir |
| 7 | +import inputs.Input.loadFileSync |
| 8 | + |
| 9 | +def loadInput(): String = loadFileSync(s"$currentDir/../input/day08") |
| 10 | + |
| 11 | + |
| 12 | +/**************/ |
| 13 | +/* Data Model */ |
| 14 | +/**************/ |
| 15 | + |
| 16 | +/** A junction box in 3D space with an associated circuit ID. */ |
| 17 | +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 | +/* Data Loading */ |
| 23 | +/****************/ |
| 24 | + |
| 25 | +/** Parses comma-separated coordinates from the given `line` into a `Box` with |
| 26 | + * the given `circuit` ID. |
| 27 | + */ |
| 28 | +def parseBox(line: String, circuit: Int): Box = |
| 29 | + val parts = line.split(",") |
| 30 | + Box(parts(0).toLong, parts(1).toLong, parts(2).toLong, circuit) |
| 31 | + |
| 32 | +/** Parses the input, returning a sequence of `Box`es and all unique pairs |
| 33 | + * of boxes sorted by distance. |
| 34 | + */ |
| 35 | +def load(input: String): (Seq[Box], Seq[(Box, Box)]) = |
| 36 | + val lines = input.linesIterator.filter(_.nonEmpty) |
| 37 | + val boxes = lines.zipWithIndex.map(parseBox).toSeq |
| 38 | + val pairsByDistance = boxes.pairs.toSeq.sortBy((b1, b2) => b1.distanceSquare(b2)) |
| 39 | + (boxes, pairsByDistance) |
| 40 | + |
| 41 | +extension [T](self: Seq[T]) |
| 42 | + /** Generates all unique pairs (combinations of 2) from the sequence. */ |
| 43 | + def pairs: Iterator[(T, T)] = |
| 44 | + self.combinations(2).map(pair => (pair(0), pair(1))) |
| 45 | + |
| 46 | + |
| 47 | +/**********/ |
| 48 | +/* Part 1 */ |
| 49 | +/**********/ |
| 50 | + |
| 51 | +@main def part1: Unit = |
| 52 | + println(s"The solution is ${part1(loadInput())}") |
| 53 | + |
| 54 | +def part1(input: String): Int = |
| 55 | + val (boxes, pairsByDistance) = load(input) |
| 56 | + for (b1, b2) <- pairsByDistance.take(1000) if b1.circuit != b2.circuit do |
| 57 | + merge(b1.circuit, b2.circuit, boxes) |
| 58 | + val sizes = boxes.groupBy(_.circuit).values.map(_.size).toSeq.sortBy(-_) |
| 59 | + sizes.take(3).product |
| 60 | + |
| 61 | +/** Sets all boxes with circuit `c2` to circuit `c1`. */ |
| 62 | +def merge(c1: Int, c2: Int, boxes: Seq[Box]): Unit = |
| 63 | + for b <- boxes if b.circuit == c2 do b.circuit = c1 |
| 64 | + |
| 65 | + |
| 66 | +/**********/ |
| 67 | +/* Part 2 */ |
| 68 | +/**********/ |
| 69 | + |
| 70 | +@main def part2: Unit = |
| 71 | + println(s"The solution is ${part2(loadInput())}") |
| 72 | + |
| 73 | +def part2(input: String): Long = |
| 74 | + val (boxes, pairsByDistance) = load(input) |
| 75 | + var n = boxes.length |
| 76 | + boundary: |
| 77 | + for (b1, b2) <- pairsByDistance if b1.circuit != b2.circuit do |
| 78 | + merge(b1.circuit, b2.circuit, boxes) |
| 79 | + n -= 1 |
| 80 | + if n <= 1 then |
| 81 | + break(b1.x * b2.x) |
| 82 | + throw Exception("Should not reach here") |
0 commit comments