|
| 1 | +# Day Twenty-Two: Reactor Reboot |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2021/day/22) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2021-clojure/blob/master/src/advent_2021_clojure/day22.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## Preamble |
| 9 | + |
| 10 | +I loved today's puzzle again! Part 1 was really simple until I realized that the algorithm wouldn't scale into the |
| 11 | +large dataset that was coming up in Part 2, so I had to do large rewrite. That said, I found this to be a very |
| 12 | +enjoyable challenge. |
| 13 | + |
| 14 | +We are given a list of reboot instructions, providing a range of cuboids with instructions on whether to set every |
| 15 | +enclosed cube on or off. After running through all the instructions in order, we need to count the number of cubes |
| 16 | +that are switched on. My original solution was to identify every cube within each line of instructions, converting |
| 17 | +them into maps of the cube to `:on` or `:off`, merge the maps together, and count the number of `:on` values. But |
| 18 | +that wasn't the final approach. |
| 19 | + |
| 20 | +My solution looked at the puzzle a little differently from the instructions. In the instructions, we start with two |
| 21 | +"on" lines; each line independently turned on 27 cubes, but since line 2 overlapped with line 1, it only added 19 cubes |
| 22 | +that weren't already on from line 1. I approached this from the other perspective - the cuboid from line 1 contains |
| 23 | +points that are all one, but the cuboid from line 1 breaks cuboid 1 into smaller cuboids that don't overlap with |
| 24 | +cuboid 2. After breaking apart all overlapping previous cuboids, the new one either joins the set of "on" cuboids, or |
| 25 | +it just quietly disappears because the new cuboid is off. |
| 26 | + |
| 27 | +Take a 2-dimensional example. We start with a square of `x=1..5,y=1..5` and we overlay it with a new square of |
| 28 | +`x=4..6,y=4..6`. Instead of breaking apart the second square, I break the first square into two smaller rectangles |
| 29 | +which don't overlap with each other or the new rectangle. Then if the new rectangle is "on," I end up with three |
| 30 | +rectangles in total; otherwise I end up with just two. |
| 31 | + |
| 32 | +``` |
| 33 | +Adding an "on" square: |
| 34 | +..... 11122 111 22 ### |
| 35 | +..... 11122 111 22 ### |
| 36 | +..... plus overlay yields 11122 which is 111 22 ### |
| 37 | +..... ### 111### 111 |
| 38 | +..... ### 111### 111 |
| 39 | + ### ### |
| 40 | + |
| 41 | +Adding an "off" square: |
| 42 | +..... 11122 111 22 |
| 43 | +..... 11122 111 22 |
| 44 | +..... plus overlay yields 11122 which is 111 22 |
| 45 | +..... ### 111 111 |
| 46 | +..... ### 111 111 |
| 47 | + ### |
| 48 | +``` |
| 49 | + |
| 50 | +This approach lets me deal only with squares, or rather cuboids when we add the third dimension back in, without |
| 51 | +having to deal with any irregular shapes. Plus the overlay logic doesn't care if the new square/cuboid is on or off |
| 52 | +until after we've dealt with the overlays. |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | +## Part 1 |
| 57 | + |
| 58 | +Alright, let's get down to business. Because we'l be looking at overlapping dimensions, the easiest way to represent |
| 59 | +a cuboid would be a map of `{:x [low high], :y [low high], :z [low high]}`, and each input instruction will be of the |
| 60 | +form `[op cuboid]`. So to parse each line of the input, we'll split the first word from the rest of the line as a |
| 61 | +keyword, identifying each dimension on the right using a neat regular expression I saw online a few days ago: |
| 62 | +`#"\-?\d+"`. This captures both the optional negative sign and also any number of numeric digits, and `read-string` can |
| 63 | +properly map those values to Longs. |
| 64 | + |
| 65 | +Note that as a matter of convention in my solution, I'll usually use `x0` and `x1` to represent `x-low` and `x-high`. |
| 66 | +If I have two cuboids where I'm examining both of their min and max values, I'll use `x0a` and `x1a` for the first |
| 67 | +cuboid, and `x0b` and `x1b` for the second. |
| 68 | + |
| 69 | +```clojure |
| 70 | +(defn parse-instruction [line] |
| 71 | + (let [[instruction dim-str] (str/split line #" ") |
| 72 | + [x0 x1 y0 y1 z0 z1] (map read-string (re-seq #"\-?\d+" dim-str))] |
| 73 | + [(keyword instruction) {:x [x0 x1], :y [y0 y1], :z [z0 z1]}])) |
| 74 | +``` |
| 75 | + |
| 76 | +We know that we'll need to discard any input instruction whose cuboid lies outside the initialization area, which is |
| 77 | +defined to the cuboids between -50 and 50. `within-initialization-area?` looks at the value range for each dimension |
| 78 | +of a cuboid, and calls `every?` to check if the values are within the required area. |
| 79 | + |
| 80 | +```clojure |
| 81 | +(def dimensions [:x :y :z]) |
| 82 | + |
| 83 | +(defn within-initialization-area? [cuboid] |
| 84 | + (letfn [(dim-ok? [dim] (let [[v0 v1] (dim cuboid)] |
| 85 | + (and (>= v0 -50) (<= v1 50))))] |
| 86 | + (every? dim-ok? dimensions))) |
| 87 | +``` |
| 88 | + |
| 89 | +Next we implement another helper function called `overlap?`, which compares two cuboids either for a single dimension |
| 90 | +or for all of them. Cuboids overlap in a dimension if each min value is not greater than the other's max value. Cuboids |
| 91 | +overlap if all of their dimensions overlap. |
| 92 | + |
| 93 | +```clojure |
| 94 | +(defn overlap? |
| 95 | + ([cuboid1 cuboid2] (every? (partial overlap? cuboid1 cuboid2) dimensions)) |
| 96 | + ([cuboid1 cuboid2 dim] (let [[v0a v1a] (dim cuboid1) |
| 97 | + [v0b v1b] (dim cuboid2)] |
| 98 | + (and (<= v0a v1b) (<= v0b v1a))))) |
| 99 | +``` |
| 100 | + |
| 101 | +Now we get to the first of two meaty functions - `split-on-dimension`. This takes in two cuboids and a dimension, and |
| 102 | +breaks the first cuboid apart into overlapping and non-overlapping cuboids. In my mind, non-overlapping cuboids are |
| 103 | +"safe," because they can't be affected by the invading cuboid, while overlapping ones are "unsafe," so the function |
| 104 | +returns a map of `{:safe cuboids, :unsafe cuboids}`. First off, if the two cuboids don't overlap in the dimension, |
| 105 | +the first cuboid is safe, so return a map of `:safe` to the vector of the cuboid itself. If they do overlap, we |
| 106 | +calculate `overlap0` and `overlap1` as the min and max values that overlap, recognizing that we should get the same |
| 107 | +values if we were to flip the order of the cuboids; the `:unsafe` region will be the original cuboid with this region |
| 108 | +set at the correct dimension. Then we look on both sides of this unsafe region to see if they're still part of the |
| 109 | +original cuboid, and only add them in if they are. |
| 110 | + |
| 111 | +For example, imagine we are looking in two dimensions again, and we call |
| 112 | +`(split-on-dimension {:x [1 5], :y [10 20]} {:x [3 5], :y [1 100]} :x)`. Looking at just the `x` values, we can see |
| 113 | +that cuboid1 has values 1 through 5 while cuboid2 has values 3 through 5. The overlap is therefore `[3 5]`, so we'll |
| 114 | +want to return `{:unsafe [{:x [3 5], :y [10 20]}]}`. But what about the area to the left and right of it? The left |
| 115 | +value would be `{:x [1 2], :y [10 20]}` which is still within the original cuboid, so that's fine. The right value |
| 116 | +would be `{:x [6 5], :y [10 20]}` since the overlap region includes `x=6`, and this range doesn't make any sense since |
| 117 | +the min `x` value is greater than the max, so we discard it. |
| 118 | + |
| 119 | +```clojure |
| 120 | +(defn split-on-dimension [cuboid1 cuboid2 dim] |
| 121 | + (if-not (overlap? cuboid1 cuboid2 dim) |
| 122 | + {:safe [cuboid1]} |
| 123 | + (let [[v0a v1a] (dim cuboid1) |
| 124 | + [v0b v1b] (dim cuboid2) |
| 125 | + overlap0 (max v0a v0b) |
| 126 | + overlap1 (min v1a v1b) |
| 127 | + safe-regions (filter (partial apply <=) [[v0a (dec overlap0)] [(inc overlap1) v1a]]) |
| 128 | + overlap-region [overlap0 overlap1]] |
| 129 | + {:safe (map #(assoc cuboid1 dim %) safe-regions) |
| 130 | + :unsafe [(assoc cuboid1 dim overlap-region)]}))) |
| 131 | +``` |
| 132 | + |
| 133 | +Hopefully that previous function made sense, because we're about to leverage it for `remove-overlaps`. Here, we again |
| 134 | +check if the two cuboids overlap at all; if not, we just return the vector of the first cuboid, since there's no reason |
| 135 | +to break it apart. (Note that with this check, we don't actually have to check `overlap?` in `split-on-dimension`, but |
| 136 | +it's a cheap check to make sure the data is always valid.) If the cuboids do overlap, then we're going to use a `reduce` |
| 137 | +over the three dimensions, looking at the set of safe and unsafe cuboids. Initially, the first cuboid is considered |
| 138 | +unsafe since we haven't examined it over overlaps. For each dimension, we compare each of the unsafe regions to the |
| 139 | +new cuboid, merging together all of their safe and unsafe cuboids for that dimension. If we identified any new safe |
| 140 | +cuboids, we'll add them to collection of previously reviewed ones. However, we _replace_ the previous unsafe regions |
| 141 | +with the new ones, since a previously unsafe region may have been split apart by `split-on-dimension`. When all is |
| 142 | +said and done, `remove-overlaps` just returns the sequence of safe regions, as region still unsafe must fully overlap |
| 143 | +with cuboid2. |
| 144 | + |
| 145 | +```clojure |
| 146 | +(defn remove-overlaps [cuboid1 cuboid2] |
| 147 | + (if-not (overlap? cuboid1 cuboid2) |
| 148 | + [cuboid1] |
| 149 | + (first (reduce (fn [[acc-safe acc-unsafe] dim] |
| 150 | + (let [{:keys [safe unsafe]} |
| 151 | + (->> acc-unsafe |
| 152 | + (map #(split-on-dimension % cuboid2 dim)) |
| 153 | + (apply merge-with conj))] |
| 154 | + [(apply conj acc-safe safe) unsafe])) |
| 155 | + [() [cuboid1]] |
| 156 | + dimensions)))) |
| 157 | +``` |
| 158 | + |
| 159 | +The worst is behind us now. We'll make three small functions that should build up into running all instructions from |
| 160 | +the input data set. First, `remove-all-overlaps` takes in a sequence of cuboids and a new cuboid, and uses `reduce` to |
| 161 | +join together all safe regions after removing their overlaps from the new cuboid. Then `apply-instruction` takes in the |
| 162 | +cuboids and an instruction, which again is an operation and a cuboid. After stripping away all overlaps, this function |
| 163 | +either returns the remaining safe regions for an `:off` instruction, or adds the new cuboid to the safe regions for an |
| 164 | +`:on` instruction. Finally, `apply-instructions` just calls `apply-instruction` on all instructions, starting with an |
| 165 | +empty collection of safe cuboids, since initially every cube is off. |
| 166 | + |
| 167 | +```clojure |
| 168 | +(defn remove-all-overlaps [cuboids new-cuboid] |
| 169 | + (reduce (fn [acc c] (apply conj acc (remove-overlaps c new-cuboid))) |
| 170 | + () |
| 171 | + cuboids)) |
| 172 | + |
| 173 | +(defn apply-instruction [cuboids [op new-cuboid]] |
| 174 | + (let [remaining (remove-all-overlaps cuboids new-cuboid)] |
| 175 | + (if (= op :on) |
| 176 | + (conj remaining new-cuboid) |
| 177 | + remaining))) |
| 178 | + |
| 179 | +(defn apply-instructions [instructions] |
| 180 | + (reduce apply-instruction () instructions)) |
| 181 | +``` |
| 182 | + |
| 183 | +Almost done! Once we've run through all instructions, we need to know how many cubes are on. For this, the |
| 184 | +`cuboid-size` function looks at each dimension within a cuboid, multiplying their lengths together. Remember that all |
| 185 | +dimensions in this problem are inclusive on both ends, so the length of `[4 6]` is 3, not 2. |
| 186 | + |
| 187 | +```clojure |
| 188 | +(defn cuboid-size [cuboid] |
| 189 | + (->> (map (fn [dim] (let [[v0 v1] (cuboid dim)] |
| 190 | + (inc (- v1 v0)))) dimensions) |
| 191 | + (apply *))) |
| 192 | +``` |
| 193 | + |
| 194 | +Alright, let's finish up with the `part1` function already! We'll read each line of the input, and map it to its |
| 195 | +parsed instruction. We then need to filter out the ones whose cuboids are within the initialization area. Then with |
| 196 | +only valid instructions, we'll apply them all together, map the size of each resulting cuboid, and add them together. |
| 197 | +Done! |
| 198 | + |
| 199 | +```clojure |
| 200 | +(defn part1 [input] |
| 201 | + (->> (str/split-lines input) |
| 202 | + (map parse-instruction) |
| 203 | + (filter #(within-initialization-area? (second %))) |
| 204 | + apply-instructions |
| 205 | + (map cuboid-size) |
| 206 | + (apply +))) |
| 207 | +``` |
| 208 | + |
| 209 | +--- |
| 210 | + |
| 211 | +## Part 2 |
| 212 | + |
| 213 | +Ok, so part 2 is the same as part 1, except that we can use all cuboids, not just the ones in the initialization area. |
| 214 | +So we'll just pull out most of the logic from `part1` into a `solve` function, which gets invoked with the input data |
| 215 | +and a filter function to apply to the initial cuboids. For part 1, we'll pass in the `within-initialization-area?` |
| 216 | +function again, while for part 2 we can ust `identity` to keep all of the input. |
| 217 | + |
| 218 | +```clojure |
| 219 | +(defn solve [instruction-filter input] |
| 220 | + (->> (str/split-lines input) |
| 221 | + (map parse-instruction) |
| 222 | + (filter #(instruction-filter (second %))) |
| 223 | + apply-instructions |
| 224 | + (map cuboid-size) |
| 225 | + (apply +))) |
| 226 | + |
| 227 | +(defn part1 [input] (solve within-initialization-area? input)) |
| 228 | +(defn part2 [input] (solve identity input)) |
| 229 | +``` |
| 230 | + |
| 231 | +That's it! Pretty fun little puzzle today. |
0 commit comments