|
| 1 | +# Day Twenty: Trench Map |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2021/day/20) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2021-clojure/blob/master/src/advent_2021_clojure/day20.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## Part 1 |
| 9 | + |
| 10 | +Today we're simulating a Game Of Life as represented by an image whose pixels are either light or dark with each |
| 11 | +generation. Our goal is to run the game a certain number of times against an image atop an infinite background, and |
| 12 | +then count up the number of lit pixels in the end. |
| 13 | + |
| 14 | +To start off, let's parse the input, which comes in as a single line of the 512-character "algorithm," followed by |
| 15 | +the initial image. We'll just return that as a two-element vector, leveraging `utils/split-blank-line` to split the |
| 16 | +input into its two segments. For the algorithm, we'll make a `pixel-map` that converts `.` to zero and `#` to 1; calling |
| 17 | +`mapv` will ensure the result is a an indexed vector instead of a linked list. For the image, we'll want that to be a |
| 18 | +map of `{[x y] 0-or-1}`. We'll leverage our existing `point/parse-to-char-coords` to return a sequence of each |
| 19 | +coordinate pair to its character, and then associate the coords onto the resulting map by again mapping the character |
| 20 | +using the `pixel-map`. Naturally we'll use constants of `dark-pixel` and `light-pixel` to avoid repeating `0` and `1` |
| 21 | +throughout the solution. |
| 22 | + |
| 23 | +```clojure |
| 24 | +(def dark-pixel 0) |
| 25 | +(def light-pixel 1) |
| 26 | + |
| 27 | +(defn parse-input [input] |
| 28 | + (let [pixel-map {\. dark-pixel, \# light-pixel} |
| 29 | + [alg image] (utils/split-blank-line input)] |
| 30 | + [(mapv pixel-map alg) |
| 31 | + (reduce (fn [m [coord c]] (assoc m coord (pixel-map c))) |
| 32 | + {} |
| 33 | + (point/parse-to-char-coords image))])) |
| 34 | +``` |
| 35 | + |
| 36 | +Our goal will include going to every point in the current image, looking at its surrounding 3x3 grid, and construct a |
| 37 | +new image from the converted characters. But what do we do along the border? Well initially, every surrounding point |
| 38 | +will be dark (aka `0`), but the algorithm tells us whether that will be true for every generation. To determine this, |
| 39 | +we'll make an infinite `border-seq`, which takes in the algorithm and returns what every infinite border value will be |
| 40 | +in each generation. Imagining an initial point far away from our image; all nine points in its grid will be dark, |
| 41 | +meaning that its binary value will be `000000000` or just plain `0`. Some algorithms will map a `0` to `0` again, |
| 42 | +keeping the border dark, but some might map it to `1` to make it light. Then similarly, a distant point of all light |
| 43 | +values will have a binary value of `111111111` or `511`, which could again map each character to dark or light. So |
| 44 | +`block-of` will map `0` to `0`, and `1` to `511`, and we'll `iterate` on mapping each previous value to its block |
| 45 | +value, and then use `alg` to convert it to its new value at that index. |
| 46 | + |
| 47 | +For what it's worth, in the sample problem, `(get alg 0)` is zero, so the border is always dark. In my input, |
| 48 | +`(map alg [0 511])` was `[1 0]`, so the background flickered every generation from dark to light and back again. |
| 49 | + |
| 50 | +```clojure |
| 51 | +(defn border-seq [alg] |
| 52 | + (let [block-of {dark-pixel 0, light-pixel 511}] |
| 53 | + (iterate #(-> % block-of alg) 0))) |
| 54 | +``` |
| 55 | + |
| 56 | +Now we can create a `migrate-image` function, which takes in the algorithm, the value of all border coordinates, and |
| 57 | +the current image, and returns the next image by migrating every point. To do this, we'll use a simple `reduce-kv` to |
| 58 | +migrate each coordinate in the image, calling `next-value-at` for each coordinate. The `next-value-at` looks up all |
| 59 | +points surrounding the current one, maps it to either its value on the image or else the default value of the |
| 60 | +infinite border character. Then it concatenates all 9 characters into a binary string, which it converts into a number |
| 61 | +and finds within the algorithm vector. |
| 62 | + |
| 63 | +```clojure |
| 64 | +(defn next-value-at [alg border image coords] |
| 65 | + (->> (point/surrounding true coords) |
| 66 | + (map #(or (image %) border)) |
| 67 | + (apply str) |
| 68 | + utils/parse-binary |
| 69 | + alg)) |
| 70 | + |
| 71 | +(defn migrate-image [alg border image] |
| 72 | + (reduce-kv (fn [m coord _] (assoc m coord (next-value-at alg border image coord))) |
| 73 | + {} image)) |
| 74 | +``` |
| 75 | + |
| 76 | +We want to get to a function that returns every generation of the image, but there's only one issue to address first. |
| 77 | +While `migrate-image` and `next-value-at` takes into account the infinite border, with each generation we almost |
| 78 | +certainly will have at least one point along the perimeter that interacted with the infinite border, meaning that the |
| 79 | +points just outside the previous border of the image will need to be calculated in the next generation. To account for |
| 80 | +this, with each generation we'll need to expand the image by one row at the top and bottom, and by one column on the |
| 81 | +left and right. (In theory, we could just expand the entire image once to the maximum intended size, but that pollutes |
| 82 | +what each function does.) |
| 83 | + |
| 84 | +The `expand-image` function looks at the current min and max values for both `x` and `y`, and associates the border |
| 85 | +character to all coordinates of the surrounding perimeter. We implement `perimeter-points` in the `point` namespace |
| 86 | +by taking all `[x y]` coordinates from a top-left point to the bottom-right point. Then `expand-image` uses `reduce` |
| 87 | +to `assoc` the `border` onto each perimeter point, starting from the `image` map itself. |
| 88 | + |
| 89 | +```clojure |
| 90 | +; advent-2021-clojure.point namespace |
| 91 | +(defn perimeter-points [[x0 y0] [x1 y1]] |
| 92 | + (concat (for [x [x0 x1], y (range y0 (inc y1))] [x y]) |
| 93 | + (for [y [y0 y1], x (range (inc x0) x1)] [x y]))) |
| 94 | + |
| 95 | +; advent-2021-clojure.day20 namespace |
| 96 | +(defn expand-image [image border] |
| 97 | + (let [min-max (juxt (partial apply min) (partial apply max)) |
| 98 | + [min-x max-x] (min-max (map ffirst image)) |
| 99 | + [min-y max-y] (min-max (map (comp second first) image))] |
| 100 | + (reduce #(assoc %1 %2 border) |
| 101 | + image |
| 102 | + (point/perimeter-points [(dec min-x) (dec min-y)] |
| 103 | + [(inc max-x) (inc max-y)])))) |
| 104 | +``` |
| 105 | + |
| 106 | +So where are we now? We know the sequence of values of the infinite border, and we can use that sequence to both |
| 107 | +expand the current image one step into its perimeter, and then to migrate every point of the image based on its |
| 108 | +neighbors. The only major step remaining is to create `image-seq`, to generate an infinite sequence of every generation |
| 109 | +the image goes through as it migrates. This function takes in the algorithm and initial image, and then creates the |
| 110 | +`border-seq` in the background. For each generation, is expands and migrates the image on the current head of the |
| 111 | +border sequence, and then uses `lazy-seq` (similar to `iterate`) to create the next element of the sequence. We don't |
| 112 | +use `iterate` here because we can't use the same value for the border with each generation, but rather need to call |
| 113 | +`(rest borders)` each time. |
| 114 | + |
| 115 | +```clojure |
| 116 | +(defn image-seq |
| 117 | + ([alg image] (image-seq alg image (border-seq alg))) |
| 118 | + ([alg image borders] (let [border (first borders) |
| 119 | + image' (->> (expand-image image border) |
| 120 | + (migrate-image alg border))] |
| 121 | + (lazy-seq (cons image' (image-seq alg image' (rest borders))))))) |
| 122 | +``` |
| 123 | + |
| 124 | +Alright, let's create our `solve` function. We'll take in the input string and the number of enhancements we want to |
| 125 | +apply. The function parses the input, converts it into the sequence of images, and finds the `nth` value by dropping |
| 126 | +`(dec enhance-count)` values from the sequence and pulling the next value. From that image, it counts the number of |
| 127 | +values in the `[[x y] pixel]` pair by counting the number of pixels that are `light-pixel`, or `1`. |
| 128 | + |
| 129 | +```clojure |
| 130 | +(defn solve [input enhance-count] |
| 131 | + (->> (parse-input input) |
| 132 | + (apply image-seq) |
| 133 | + (drop (dec enhance-count)) |
| 134 | + first |
| 135 | + (filter #(= light-pixel (second %))) |
| 136 | + count)) |
| 137 | +``` |
| 138 | + |
| 139 | +Finally, `part1` just calls `solve`, looking for the number of pixels after the second enhancement. |
| 140 | +```clojure |
| 141 | +(defn part1 [input] (solve input 2)) |
| 142 | +``` |
| 143 | + |
| 144 | +--- |
| 145 | + |
| 146 | +## Part 2 |
| 147 | + |
| 148 | +Part 2 runs the same algorithm for fifty enhancements. The code we already have is fine; we'll get the answer in about |
| 149 | +10-15 seconds, which is fast enough for me. |
| 150 | + |
| 151 | +```clojure |
| 152 | +(defn part2 [input] (solve input 50)) |
| 153 | +``` |
0 commit comments