Skip to content

Commit 6fe239f

Browse files
committed
Day 11 docs
1 parent 8cf8e42 commit 6fe239f

File tree

3 files changed

+221
-23
lines changed

3 files changed

+221
-23
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ _Warning:_ I write long explanations. So... yeah.
2020
| 8 | [source](src/advent_2020_clojure/day08.clj) | [blog](docs/day08.md) |
2121
| 9 | [source](src/advent_2020_clojure/day09.clj) | [blog](docs/day09.md) |
2222
| 10 | [source](src/advent_2020_clojure/day10.clj) | [blog](docs/day10.md) |
23+
| 11 | [source](src/advent_2020_clojure/day11.clj) | [blog](docs/day11.md) |

docs/day11.md

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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+

src/advent_2020_clojure/day11.clj

+23-23
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,62 @@
22
(:require [clojure.string :as str]
33
[advent-2020-clojure.utils :refer [mapv-indexed]]))
44

5-
(defn occupied-seat? [c] (= \# c))
6-
(defn empty-seat? [c] (= \L c))
5+
(def occupied-seat \#)
6+
(def empty-seat \L)
7+
(def space \.)
8+
(defn occupied-seat? [c] (= occupied-seat c))
9+
(defn empty-seat? [c] (= empty-seat c))
710
(defn seat? [c] (or (occupied-seat? c) (empty-seat? c)))
811

912
(def all-directions '([-1 -1] [-1 0] [-1 1]
1013
[0 -1] [0 1]
1114
[1 -1] [1 0] [1 1]))
1215

13-
(defn all-neighbor-paths [point grid]
16+
(defn all-neighbor-paths [grid point]
1417
(let [rows (count grid)
1518
cols (count (first grid))
1619
in-range? (fn [[y x]] (and (< -1 y rows)
1720
(< -1 x cols)))]
1821
(for [dir all-directions]
1922
(->> (iterate (partial map + dir) point)
2023
rest
21-
(take-while in-range?)
22-
(map vec)))))
24+
(take-while in-range?)))))
2325

2426
(defn first-in-path [grid f path]
2527
(->> path
2628
(map (partial get-in grid))
2729
(filter f)
2830
first))
2931

30-
(defn occupied-neighbors-by [point grid f]
31-
(->> (all-neighbor-paths point grid)
32+
(defn occupied-neighbors-by [grid point f]
33+
(->> (all-neighbor-paths grid point)
3234
(map (partial first-in-path grid f))
3335
(filter occupied-seat?)
3436
count))
3537

36-
(defn next-point [point grid f awkwardness]
37-
(let [c (get-in grid point)
38-
occ (occupied-neighbors-by point grid f)]
39-
(cond
40-
(and (empty-seat? c) (zero? occ)) \#
41-
(and (occupied-seat? c) (>= occ awkwardness)) \L
42-
:else c)))
38+
(defn next-point [grid point f awkwardness]
39+
(let [c (get-in grid point)]
40+
(if-not (seat? c)
41+
space
42+
(let [occ (occupied-neighbors-by grid point f)]
43+
(cond
44+
(and (empty-seat? c) (zero? occ)) occupied-seat
45+
(and (occupied-seat? c) (>= occ awkwardness)) empty-seat
46+
:else c)))))
4347

4448
(defn next-turn [grid f awkwardness]
45-
(->> grid
46-
(mapv-indexed (fn [y row]
47-
(->> row
48-
(mapv-indexed (fn [x _]
49-
(next-point [y x] grid f awkwardness)))
50-
(apply str))))))
51-
49+
(mapv-indexed (fn [y row]
50+
(mapv-indexed (fn [x _]
51+
(next-point grid [y x] f awkwardness))
52+
row))
53+
grid))
5254

5355
(defn solve [input f awkwardness]
5456
(->> (iterate #(next-turn % f awkwardness)
5557
(str/split-lines input))
5658
(partition 2 1)
57-
(drop 1)
5859
(filter (partial apply =))
5960
ffirst
60-
flatten
6161
(apply str)
6262
(filter occupied-seat?)
6363
count))

0 commit comments

Comments
 (0)