|
| 1 | +# Day Ninteen: Beacon Scanner |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2021/day/19) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2021-clojure/blob/master/src/advent_2021_clojure/day19.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## Preamble |
| 9 | + |
| 10 | +I'll just come out and say that this problem was too hard for a single day. I could see this having been split into 2 |
| 11 | +or even 3 different problems, given the complexities of both the background and the puzzle itself. I have a working |
| 12 | +solution that's pretty slow, but I got there. |
| 13 | + |
| 14 | +One of my biggest struggles with this problem was understanding the nature of the beacons and the scanners. I |
| 15 | +improperly spent a large amount of time having created the space of beacons around the scanner, and then attempted to |
| 16 | +rotate the entire space. This is not how the problem is intended to be interpretted. Instead, one needs to hold the |
| 17 | +beacons steady, and then change the way that the scanner is pointing. So the trick was to think of the beacons as |
| 18 | +normal points within a cube, and to imagine the scanner as a six-sided die with only a single point having dots. |
| 19 | + |
| 20 | +Once I got that, I was at least closer to a working solution. But teasing it apart piece by piece, the puzzle is |
| 21 | +doable. |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## Part 1 |
| 26 | + |
| 27 | +First, let's parse our input. Splitting the data into groups of lines, I'll transform each block of text lines into a |
| 28 | +scanner of form `{:id n, :beacons #{[x y z]}, :scanners #{[0 0 0]}}`. The ID comes from the header line, the beacons |
| 29 | +from the rest of the data, and the scanners will be a set of a three-dimensional origin, since the scanners begin not |
| 30 | +having any context to each other. Finally, the parser will map each ID to its scanner, thus ending with a total |
| 31 | +structure of `{n {:id n, :beacons #{[x y z]}, :scanners #{[0 0 0]}}`. |
| 32 | + |
| 33 | +```clojure |
| 34 | +(defn parse-scanner [input] |
| 35 | + (let [[header & beacons] (str/split-lines input)] |
| 36 | + {:id (->> header (re-seq #"\d+") first parse-int) |
| 37 | + :beacons (->> beacons |
| 38 | + (map (fn [line] (mapv parse-int (str/split line #",")))) |
| 39 | + set) |
| 40 | + :scanners #{[0 0 0]}})) |
| 41 | + |
| 42 | +(defn parse-input [input] |
| 43 | + (reduce #(assoc %1 (:id %2) %2) |
| 44 | + {} |
| 45 | + (map parse-scanner (utils/split-blank-line input)))) |
| 46 | +``` |
| 47 | + |
| 48 | +Next, we'll make a vector of 24 transformational functions, which represent the different ways that the _scanner_ can |
| 49 | +face. Again, this has nothing to do with the beacons; it's all about the scanner. To that end, there are six groups of |
| 50 | +four functions, signifying the six directions the scanner can face, and the four rotations it can make within each |
| 51 | +direction. Each of these functions takes in an `[x y z]` point, and returns how the scanner would transform that point |
| 52 | +based on its orientation. I was only able to come up with this vector by placing and rotating objects within my house, |
| 53 | +so I could see how all 3 dimensions work together. |
| 54 | + |
| 55 | +```clojure |
| 56 | +(def orientation-fns [; Face straight |
| 57 | + identity |
| 58 | + (fn [[x y z]] [(- z) y x]) |
| 59 | + (fn [[x y z]] [(- x) y (- z)]) |
| 60 | + (fn [[x y z]] [z y (- x)]) |
| 61 | + |
| 62 | + ; Face right |
| 63 | + (fn [[x y z]] [(- y) x z]) |
| 64 | + (fn [[x y z]] [(- z) x (- y)]) |
| 65 | + (fn [[x y z]] [y x (- z)]) |
| 66 | + (fn [[x y z]] [z x y]) |
| 67 | + |
| 68 | + ; Face behind |
| 69 | + (fn [[x y z]] [(- x) (- y) z]) |
| 70 | + (fn [[x y z]] [(- z) (- y) (- x)]) |
| 71 | + (fn [[x y z]] [x (- y) (- z)]) |
| 72 | + (fn [[x y z]] [z (- y) x]) |
| 73 | + |
| 74 | + ; Face left |
| 75 | + (fn [[x y z]] [y (- x) z]) |
| 76 | + (fn [[x y z]] [(- z) (- x) y]) |
| 77 | + (fn [[x y z]] [(- y) (- x) (- z)]) |
| 78 | + (fn [[x y z]] [z (- x) (- y)]) |
| 79 | + |
| 80 | + ; Face up |
| 81 | + (fn [[x y z]] [x z (- y)]) |
| 82 | + (fn [[x y z]] [y z x]) |
| 83 | + (fn [[x y z]] [(- x) z y]) |
| 84 | + (fn [[x y z]] [(- y) z (- x)]) |
| 85 | + |
| 86 | + ; Face down |
| 87 | + (fn [[x y z]] [x (- z) y]) |
| 88 | + (fn [[x y z]] [(- y) (- z) x]) |
| 89 | + (fn [[x y z]] [(- x) (- z) (- y)]) |
| 90 | + (fn [[x y z]] [y (- z) (- x)])]) |
| 91 | +``` |
| 92 | + |
| 93 | +Next, I'll make two helper functions for relating two points to each other. `path-to` takes in a `from` point and a |
| 94 | +`to` point, and returns the different between the two, representing the values to add to `from` to end up at `to`. |
| 95 | +Then `follow-path` takes in a point and a path from `path-to`, and moves the point along that path. |
| 96 | + |
| 97 | +```clojure |
| 98 | +(defn path-to [from to] (mapv - to from)) |
| 99 | +(defn follow-path [point path] (mapv + point path)) |
| 100 | +``` |
| 101 | + |
| 102 | +Next, I have three similar functions - `combine-beacons`, `combine-scanners`, and `combine-all-scanners`. Let's handle |
| 103 | +them one at a time. |
| 104 | + |
| 105 | +`combine-beacons` takes in two sets of beacons, from two separate scanners. Assuming the first set of beacons is the |
| 106 | +fixed set, it attempts to return the first path that would move at least 12 points from the second set onto the first. |
| 107 | +For this, we'll use `(for [b0 beacons0, b1 beacons1] (path-to b1 b0))` to return _almost_ all possible vectors for the |
| 108 | +paths from the second set of beacons onto the first. We know that at least points need to overlap, but we don't know |
| 109 | +which ones. Regardless, instead of testing all of `beacons1` against all of `beacons0`, we can test against all but any |
| 110 | +11 arbitrary values from `beacons0` instead; if we know we need at least 12 points to match, then one working path |
| 111 | +should imply at least 12 ways to get to it. Finally, to see if the path is acceptable, we call `follow-path` on all |
| 112 | +beacons in the second set, and test if the intersection against the first set if at leats 12; if not, then that path |
| 113 | +does not lead to the necessary overlap. The function then returns either the first workable path, or else `nil`. |
| 114 | + |
| 115 | +```clojure |
| 116 | +(defn combine-beacons [beacons0 beacons1] |
| 117 | + (->> (for [b0 (drop 11 beacons0), b1 beacons1] (path-to b1 b0)) |
| 118 | + (filter (fn [path] (>= (->> (map #(follow-path % path) beacons1) |
| 119 | + set |
| 120 | + (set/intersection beacons0) |
| 121 | + count) |
| 122 | + 12))) |
| 123 | + first)) |
| 124 | +``` |
| 125 | + |
| 126 | +Peeling up a layer, we get to `combine-scanners`. This looks at two scanners and their beacons, and checks to see if |
| 127 | +there is any orientation of `scanner1` such that its beacons overlap with those from `scanner0`. If so, then return |
| 128 | +what `scanner0` would look like if we merged in all of the data from `scanner1`. This means applying the orientation |
| 129 | +function to all of the points, finding the working path, moving all reoriented points along that path, and adding them |
| 130 | +to `scanner0`'s beacons. Similarly, since both scanners think themselves the origin, we'll reorient the `:scanners` in |
| 131 | +`scanner1` and send them along the path, adding the resulting values ot the set of `:scanners` in `scanner0`. If the |
| 132 | +two scanners can't overlap, again return `nil`. |
| 133 | + |
| 134 | +```clojure |
| 135 | +(defn combine-scanners [scanner0 scanner1] |
| 136 | + (let [[beacons0 beacons1] (map :beacons [scanner0 scanner1])] |
| 137 | + (first (keep (fn [f] |
| 138 | + (let [beacons1' (map f beacons1)] |
| 139 | + (when-some [path (combine-beacons beacons0 beacons1')] |
| 140 | + (-> scanner0 |
| 141 | + (update :beacons set/union (set (map #(follow-path % path) beacons1'))) |
| 142 | + (update :scanners (fn [s] (apply conj s (map #(follow-path (f %) path) (:scanners scanner1))))))))) |
| 143 | + orientation-fns)))) |
| 144 | +``` |
| 145 | + |
| 146 | +Finally, `combine-all-scanners` takes in the map of parsed scanners, and keeps combining them until there's only a |
| 147 | +single scanner remaining. It starts by looking at all unique pairs of IDs in incrementing value, since there's no need |
| 148 | +to join scanner 2 to scanner 3 if we can't join scanner 3 to scanner 2. Then for each pair, if they can be joined, then |
| 149 | +remove the second scanner and update the first one to have the combined data. After going through all possible pairs, |
| 150 | +recurse back through the function again until there's only one scanner remaining. Note that since this function takes a |
| 151 | +long time to run, I left in some `println` statements to show progress. |
| 152 | + |
| 153 | +```clojure |
| 154 | +(defn combine-all-scanners [scanners] |
| 155 | + (println "Examining" (count scanners) "scanners") |
| 156 | + (if (= (count scanners) 1) |
| 157 | + (scanners 0) |
| 158 | + (recur (reduce (fn [acc [id0 id1]] (if-not (and (acc id0) (acc id1)) |
| 159 | + acc |
| 160 | + (if-some [scanner' (combine-scanners (acc id0) (acc id1))] |
| 161 | + (do (println "Combining" id0 "with" id1 "from keys" (keys acc)) |
| 162 | + (-> acc |
| 163 | + (dissoc id1) |
| 164 | + (assoc id0 scanner'))) |
| 165 | + acc))) |
| 166 | + scanners |
| 167 | + (for [id0 (keys scanners), id1 (keys scanners), :when (> id1 id0)] [id0 id1]))))) |
| 168 | +``` |
| 169 | + |
| 170 | +Finally, for `part1`, we just have to count the number of beacons remaining in the merged scanner. So parse the |
| 171 | +data, combine the scanners, and get the count of beacons. |
| 172 | + |
| 173 | +```clojure |
| 174 | +(defn part1 [input] (->> input parse-input combine-all-scanners :beacons count)) |
| 175 | +``` |
| 176 | + |
| 177 | +--- |
| 178 | + |
| 179 | +## Part 2 |
| 180 | + |
| 181 | +For part 2, we need to again combine all of the scanners, but then calculate the maximum distance between any two |
| 182 | +scanners. For this, we'll implement a three-dimensional `manhattan-distance` function, which computes the path between |
| 183 | +two points, and adds together the sum of each path's dimension. Then we pair together each set of scanner points, |
| 184 | +calculate the `manhattan-distance`, and return the largest value seen. |
| 185 | + |
| 186 | +```clojure |
| 187 | +(defn manhattan-distance [p1 p2] |
| 188 | + (->> (path-to p1 p2) |
| 189 | + (map utils/abs) |
| 190 | + (apply +))) |
| 191 | + |
| 192 | +(defn greatest-distances [s] |
| 193 | + (->> (for [v1 s, v2 s, :when (not= v1 v2)] [v1 v2]) |
| 194 | + (map (partial apply manhattan-distance)) |
| 195 | + (apply max))) |
| 196 | + |
| 197 | +(defn part2 [input] (->> input parse-input combine-all-scanners :scanners greatest-distances)) |
| 198 | +``` |
| 199 | + |
| 200 | +So yeah - it's not a whole lot of code once we're all said and done, but I found the puzzle very difficult to |
| 201 | +understand. Still, it's done! The code is slow and verbose, but it's done! |
0 commit comments