|
| 1 | +# Day Eleven: Seating System |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2020/day/11) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2020-clojure/blob/master/src/advent_2020_clojure/day11.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +Well the problems are officially becoming more difficult. Or perhaps my solutions are just becoming messier. |
| 9 | +Either way, problem 11 is done and there's something to show for it. Because I like to solve parts 1 and 2 |
| 10 | +in very similar ways, I'm going to describe both problem statements together up-front, which will explain |
| 11 | +why some functions are a bit more parameterized (read: complex). |
| 12 | + |
| 13 | +We are given a grid of positions within a floor, where each character is one of three characters: `.` for |
| 14 | +a blank/unused space, `L` for an empty seat, or `#` for an occupied seat. The program requires looking at |
| 15 | +every seat (empty or occupied) within the floor, deciding how many seats in each direction are considered |
| 16 | +occupied, and then decide whether an occupied seat should be emptied, and/or an empty seat should be |
| 17 | +occupied. There are two parameters that distinguish part 1 from part 2. First, the algorithm to apply to |
| 18 | +decide which seat to inspect in each direction when deciding if it's occupied. And second, what I call the |
| 19 | +`awkwardness,` which says how many neighboring occupied seats is enough to encourage someone in that seat |
| 20 | +to empty it. |
| 21 | + |
| 22 | +--- |
| 23 | + |
| 24 | +# Part 1 |
| 25 | + |
| 26 | +The lion's share of the work goes into part 1, given that we're going to parameterize the "looking function" |
| 27 | +and the awkwardness. |
| 28 | + |
| 29 | +First, I prepare a few simple definitions to make the logic more business-y. Namely, I don't want to |
| 30 | +pepper the code with the characters `.` and `L` and `#`, so I make constants `occupied-seat`, `empty-seat`, |
| 31 | +and `space`, and predicates `occupied-seat?`, `empty-seat?` and `seat?` to get that out of the subsequent |
| 32 | +logic. |
| 33 | + |
| 34 | +```clojure |
| 35 | +(def occupied-seat \#) |
| 36 | +(def empty-seat \L) |
| 37 | +(def space \.) |
| 38 | +(defn occupied-seat? [c] (= occupied-seat c)) |
| 39 | +(defn empty-seat? [c] (= empty-seat c)) |
| 40 | +(defn seat? [c] (or (occupied-seat? c) (empty-seat? c))) |
| 41 | +``` |
| 42 | + |
| 43 | +Now I've decided not to do any fancy parsing with the data this time. Normally I would consider reading |
| 44 | +each line, maybe creating a map of each `[x y]` coordinate to its value, but I found it easier this time |
| 45 | +to just split the lines and consider a `grid` to be a list of Strings. Because of this, my coordinates |
| 46 | +are expressed as `[y x]` instead of `[x y]`, since `(get-in grid [y x])` works naturally, by taking the |
| 47 | +`nth` String within the `grid` as the `y` value, and then then `nth` character within the String as the |
| 48 | +`x` value. |
| 49 | + |
| 50 | +So the next big task is to take a point within a grid, and return a list of all points seen when looking |
| 51 | +in each of the 8 directions. First, I defined a var called `all-directions`, which shows the slopes of lines |
| 52 | +to follow. Note that `[0 0]` is not in the list, since we need some movement. I thought about calculating |
| 53 | +this, but sometimes a constant is just what we want. |
| 54 | + |
| 55 | +```clojure |
| 56 | +(def all-directions '([-1 -1] [-1 0] [-1 1] |
| 57 | + [0 -1] [0 1] |
| 58 | + [1 -1] [1 0] [1 1])) |
| 59 | +``` |
| 60 | + |
| 61 | +To build out the `all-neighbor-paths`, we'll use a little `for` construct. First, I'll count the number |
| 62 | +of rows and columns in the grid, by looking at `(count grid)` for the rows, and `(count (first grid))` for |
| 63 | +the columns. Note that these don't change within the program, so in theory I could have calculated them |
| 64 | +once and made the grid into a more complex data structure, but I thought this is fine. Then, I defined |
| 65 | +a helper inner function called `in-range?`, which ensures that any `[y x]` point is within the grid. |
| 66 | + |
| 67 | +Finally, I loop over all of the directions, and set up a little threading. The key to this is the expression |
| 68 | +`(iterate (partial map + dir) point)`, which returns a lazy sequence of points. Starting with the `point` |
| 69 | +that's passed in to the function, and a direction `dir`, we call `(map + dir point)` to add the slope to |
| 70 | +the point, thus "walking" in the chosen direction. Then we use `rest` to drop the first value, since it's |
| 71 | +the originating point, and `(take-while in-range?)` to keep only the values that fall within the map. |
| 72 | + |
| 73 | +So this should return a list of 8 element, each being a list of `[y x]` points. |
| 74 | + |
| 75 | +```clojure |
| 76 | +(defn all-neighbor-paths [grid point] |
| 77 | + (let [rows (count grid) |
| 78 | + cols (count (first grid)) |
| 79 | + in-range? (fn [[y x]] (and (< -1 y rows) |
| 80 | + (< -1 x cols)))] |
| 81 | + (for [dir all-directions] |
| 82 | + (->> (iterate (partial map + dir) point) |
| 83 | + rest |
| 84 | + (take-while in-range?))))) |
| 85 | +``` |
| 86 | + |
| 87 | +Now that we can look in all directions, we need a function `first-in-path` to return the first element |
| 88 | +with a list of `[y x]` points whose value in the grid passes our "looking function," which we can still |
| 89 | +abstract away for now. So all we do is start with the path (list of points), map each point into its |
| 90 | +value on grid using `(get-in grid point)`, filter the values by the looking function `f`, and return |
| 91 | +the first value if there is any. |
| 92 | + |
| 93 | +```clojure |
| 94 | +(defn first-in-path [grid f path] |
| 95 | + (->> path |
| 96 | + (map (partial get-in grid)) |
| 97 | + (filter f) |
| 98 | + first)) |
| 99 | +``` |
| 100 | + |
| 101 | +Ok, let's recap where we are. Given a map, a starting point, and looking function, return the value of |
| 102 | +the first point in the map that meets the looking function's needs. Now we need to see how many of these |
| 103 | +values around the point are occupied, so we'll make `occupied-neighbors-by`. to start, we'll call |
| 104 | +`all-neighbor-paths` to find the 3 to 8 neighboring paths. For each path, map it to `first-in-path` with |
| 105 | +the looking function to get back a list of spaces in the grid. Then we select out the occupied ones using |
| 106 | +`(filter occupied-seat? col)`, and `count` the results. |
| 107 | + |
| 108 | +```clojure |
| 109 | +(defn occupied-neighbors-by [grid point f] |
| 110 | + (->> (all-neighbor-paths grid point) |
| 111 | + (map (partial first-in-path grid f)) |
| 112 | + (filter occupied-seat?) |
| 113 | + count)) |
| 114 | +``` |
| 115 | + |
| 116 | +So next we want to build out the function `next-turn`, which takes a grid and returns the next grid after |
| 117 | +calculating each point. The fact that we need to do some logic on each point says we should build a |
| 118 | +`next-point` function first. This function models the core business logic about how to change a seat. |
| 119 | +I won't go line-by-line, but we figure out the current value in the grid as `c`, find out how many |
| 120 | +neighbors are occupied using `occupied-neighbors-by`, and then use those values and the `awkwardness` |
| 121 | +factor to either occupy the seat, empty it, or keep it the same. I threw in a little optimization near |
| 122 | +the top, which says that if the point isn't even a seat (it's a space), then don't do any calculations |
| 123 | +since spaces never change. |
| 124 | + |
| 125 | +```clojure |
| 126 | +(defn next-point [grid point f awkwardness] |
| 127 | + (let [c (get-in grid point)] |
| 128 | + (if-not (seat? c) |
| 129 | + space |
| 130 | + (let [occ (occupied-neighbors-by grid point f)] |
| 131 | + (cond |
| 132 | + (and (empty-seat? c) (zero? occ)) occupied-seat |
| 133 | + (and (occupied-seat? c) (>= occ awkwardness)) empty-seat |
| 134 | + :else c))))) |
| 135 | +``` |
| 136 | + |
| 137 | + |
| 138 | +Ok, _now_ we can implement `next-turn`. I thought about using nested `for` macros, but opted |
| 139 | +for nested `mapv-indexed` functions; `mapv-indexed` is a utility function I wrote to call |
| 140 | +`map-indexed` and then `vec` to get back a vector. I'm not sure why there's `map` and `mapv`, |
| 141 | +and `map-indexed` but no `mapv-indexed`. Anyway, for each point we call `next-point` to |
| 142 | +calculate its value, which essentially gives us the next grid. |
| 143 | + |
| 144 | +```clojure |
| 145 | +(defn next-turn [grid f awkwardness] |
| 146 | + (mapv-indexed (fn [y row] |
| 147 | + (mapv-indexed (fn [x _] |
| 148 | + (next-point grid [y x] f awkwardness)) |
| 149 | + row)) |
| 150 | + grid)) |
| 151 | +``` |
| 152 | + |
| 153 | +At least, the meat of the problem. We want to run `next-turn` until the grid from run `n` is |
| 154 | +the same as the grid from run `n+1`. Again, `iterate` is the work horse, taking the grid |
| 155 | +we get from just calling `(str/split-lines input)` and feeding it in to the `next-turn` |
| 156 | +function for a lazy sequence. `(partition 2 1 col)` will return every pair of elements and |
| 157 | +the next element, which we then filter for equality to see if the grid has stablized. If so, |
| 158 | +we call `ffirst`, which is the same as `(first (first))` to get the first such pair of |
| 159 | +identical values, and to then pull out the first grid. Then it's smooth sailing from here - |
| 160 | +flatten and call `str` to get one big String, filter every character for only the occupied |
| 161 | +ones, and count them up. |
| 162 | + |
| 163 | +```clojure |
| 164 | +(defn solve [input f awkwardness] |
| 165 | + (->> (iterate #(next-turn % f awkwardness) |
| 166 | + (str/split-lines input)) |
| 167 | + (partition 2 1) |
| 168 | + (filter (partial apply =)) |
| 169 | + ffirst |
| 170 | + (apply str) |
| 171 | + (filter occupied-seat?) |
| 172 | + count)) |
| 173 | +``` |
| 174 | + |
| 175 | +Finally, we implement our tiny `part1` function by calling `solve` with a looking function of |
| 176 | +`some?` and an awkwardness of `4`. We use `some?` because when we have a list of points in |
| 177 | +a path and we want the immediate neighbor, `some?` returns `true` if the value is not `nil`, |
| 178 | +so `(first (map some? col))` is the same as `(first col)`. |
| 179 | + |
| 180 | +```clojure |
| 181 | +(defn part1 [input] (solve input some? 4)) |
| 182 | +``` |
| 183 | + |
| 184 | +--- |
| 185 | + |
| 186 | +## Part 2 |
| 187 | + |
| 188 | +Whew, that was a lot of work. Was it worth it? |
| 189 | + |
| 190 | +```clojure |
| 191 | +(defn part2 [input] (solve input seat? 5)) |
| 192 | +``` |
| 193 | + |
| 194 | +It sure was! We use a looking function of `seat?` because when looking in a path, we'll |
| 195 | +accept any value that isn't an open space. Other than the awkwardness factor being `5`, |
| 196 | +the rest of the algorithm holds. We're done! |
| 197 | + |
0 commit comments