Skip to content

Commit c65ae94

Browse files
committed
Day 18
1 parent 2762bc8 commit c65ae94

File tree

5 files changed

+450
-0
lines changed

5 files changed

+450
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@
1919
| 15 | [source](src/advent_2021_clojure/day15.clj) | [blog](docs/day15.md) |
2020
| 16 | [source](src/advent_2021_clojure/day16.clj) | [blog](docs/day16.md) |
2121
| 17 | [source](src/advent_2021_clojure/day17.clj) | [blog](docs/day17.md) |
22+
| 18 | [source](src/advent_2021_clojure/day18.clj) | [blog](docs/day18.md) |

docs/day18.md

+239
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
# Day Eighteen: Snailfish
2+
3+
* [Problem statement](https://adventofcode.com/2021/day/18)
4+
* [Solution code](https://github.com/abyala/advent-2021-clojure/blob/master/src/advent_2021_clojure/day18.clj)
5+
6+
---
7+
8+
## Preamble
9+
10+
I found today's problem to be quite simple to work through, which was a delight for a Saturday morning exercise.
11+
Clojure definitely made a few things extra easy today, as we help our snailfish friends with their math homework.
12+
13+
We are given several snailfish numbers that need to be added together, reducing them after each addition, and then
14+
return the final magnitude calculation. I won't repeat all of the instructions for how the snail math works, but I do
15+
want to spend a moment to talk about Clojure persistent vectors, their equivalent of Java arrays or lists. These
16+
vectors implement a bunch of interfaces, including Associative, Sequential, and Indexed. This means that they are very
17+
powerful workhorses, especially in their ability to work like maps.
18+
19+
It's common in Clojure to use functions like `get`, `get-in`, `assoc`, or `update-in` to modify elements within a map.
20+
We can use all of those functions with vectors, where the key of each function is its index in the vector. In the case
21+
of nested vectors, a function like `get-in` takes in a vector of the indexes to access.
22+
23+
```clojure
24+
; Work with maps
25+
(def person {:name "Andrew" :address {:street-num 123, :street-name "Fake Street", :city "Springfield"}})
26+
=> nil
27+
28+
(get person :name)
29+
=> "Andrew"
30+
31+
(get-in person [:address :city])
32+
=> "Springfield"
33+
34+
(assoc person :name "Homer")
35+
=> {:name "Homer" :address {:street-num 123, :street-name "Fake Street", :city "Springfield"}}
36+
37+
(update-in person [:address :street-num] inc)
38+
=> {:name "Andrew" :address {:street-num 124, :street-name "Fake Street", :city "Springfield"}}
39+
40+
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;
41+
42+
; Work with vectors
43+
(def tree [[1, 2] 3])
44+
=> nil
45+
46+
(get tree 1)
47+
=> 3
48+
49+
(get-in tree [0 1])
50+
=> 2
51+
52+
(assoc tree 0 5)
53+
=> [5 3]
54+
55+
(update-in tree [0 0] dec)
56+
=> [[0 2] 3]
57+
```
58+
59+
Alright, with that lesson out of the way, let's do some math!
60+
61+
---
62+
63+
## Part 1
64+
65+
66+
First of all, parsing - there's almost nothing to do. Whether we use Clojure's core `read-string` function or the
67+
`edn/read-string` function, either can turn a string `[[1,2],3]` into a two element vector of a pair of longs and a
68+
long. How convenient for us, since I'm quite happy to represent a snailfish number (which I call "trees" in my code)
69+
as a nested vector. So check out this rough parsing logic.
70+
71+
```clojure
72+
(defn parse-trees [input] (map read-string (str/split-lines input)))
73+
```
74+
75+
### Exploding
76+
77+
Now let's think about how to explode a tree. Calm down, we're just going to explode a snailfish number within a tree,
78+
but `explode-snailfish-number-within-a-tree` is ridiculous. Without keeping track of every node within the tree, its
79+
value(s) and depth, I'm just going to walk through the tree instead. The `explode-pos` function takes in a tree and
80+
returns the vector position of which node can be exploded, if any. To do this, the function will recursively call it
81+
with different `pos` values, representing the index in the tree to examine, in a depth-first search. If there are four
82+
elements within `pos`, then we've reached explosion depth, so return that `pos` vector itself. Otherwise, look at the
83+
two children in the tree at that index, recursing into `explode-pos` if the leaf node is a vector; we don't explode
84+
regular numbers, only vector nodes.
85+
86+
```clojure
87+
(defn explode-pos
88+
([tree] (explode-pos tree []))
89+
([tree pos] (if (>= (count pos) 4)
90+
pos
91+
(->> (get-in tree pos)
92+
(keep-indexed (fn [idx leaf] (when (vector? leaf)
93+
(explode-pos tree (conj pos idx)))))
94+
first))))
95+
```
96+
97+
So let's assume we find an `explode-pos` for a tree. That position will become a regular zero, but we need to find the
98+
closest regular numbers on either side, or at least the positions of them. For this, we'll make two functions called
99+
`regular-numbers` and `regular-number-positions`. The former will take in a tree and return a sequence of `[pos val]`
100+
pairs for each regular number in the tree, representing the position vector and the value of that number. The latter
101+
just strips out the position values. Again, this is a depth-first search for _all_ regular numbers, meaning leaf nodes
102+
that aren't themselves vectors.
103+
104+
```clojure
105+
(defn regular-numbers
106+
([tree] (regular-numbers tree []))
107+
([tree pos] (->> (get-in tree pos)
108+
(map-indexed (fn [idx leaf] (let [leaf-pos (conj pos idx)]
109+
(if (number? leaf)
110+
[[leaf-pos leaf]]
111+
(regular-numbers tree leaf-pos)))))
112+
(apply concat))))
113+
114+
(defn regular-number-positions [tree] (->> tree regular-numbers (map first)))
115+
```
116+
117+
Why did we make these sequences? Because if we explode a node by converting it into a 0, and we know its location, we
118+
can then find the positions of the nodes immediately to the left and the right, with a simple function called
119+
`pos-before-and-after`. This will take in an arbitrary collection (in this case, a sequence of position vectors) and
120+
a search value, and returns a two-element vector of the values right before and right after that value. We can use an
121+
old trick of calling `(partition 3 1)` to create three-element windowed views of all values in the collection, which
122+
we call `[a b c]`, returning `[a c]` when `b` equals the search value. The only tricky part is that we'll have to add
123+
`nil` values to the front and back of the input collection, since the search value might be the first or last value,
124+
and `(partition 3 1 [:my-value :my-neighbor])` would return `nil`, when we would want `[nil :my-neighbor]`.
125+
126+
```clojure
127+
(defn pos-before-and-after [coll v]
128+
(->> (concat [nil] coll [nil])
129+
(partition 3 1)
130+
(keep (fn [[a b c]] (when (= b v) [a c])))
131+
first))
132+
```
133+
134+
It's time to write `explode-tree`, which takes in a tree and returns either the output of exploding its first node, or
135+
`nil` if it can't be exploded. We'll call `explode-pos` to get the position of the exploding node. If it's there, we'll
136+
pull out its elements `a` and `b`, then zero out the value at that position, and find the positions of the neighboring
137+
regular numbers, if any. Finally, we'll add `a` to the left neighbor, and `b` to the right neighbor. The
138+
`add-if-present` helper function adds the previous value to the neighboring number, if it's present. We'll use
139+
`(if pos then else)` structure to only attempt the `update-in` if the `pos` value is not `nil`.
140+
141+
```clojure
142+
(defn add-if-present [tree pos v]
143+
(if pos (update-in tree pos + v) tree))
144+
145+
(defn explode-tree [tree]
146+
(when-some [pos (explode-pos tree)]
147+
(let [[a b] (get-in tree pos)
148+
zeroed-out (assoc-in tree pos 0)
149+
[left-pos right-pos] (-> (regular-number-positions zeroed-out)
150+
(pos-before-and-after pos))]
151+
(-> zeroed-out
152+
(add-if-present left-pos a)
153+
(add-if-present right-pos b)))))
154+
```
155+
156+
### Splitting
157+
158+
Splitting a node in the tree is much easier to do, now that we've got all the pre-work done for explosions. Just as we
159+
made `explode-pos`, we'll make `split-pos` to find the position of the first regular number to be split, meaning its
160+
value is at or above 10. Using `regular-numbers`, we just return the position of the first such value, so that's easy.
161+
To split the value there, we'll make `split-val`, which makes a vector of half of the value rounded-down, and the
162+
difference between it and the original value; this was easier for me than rounding up. Then `split-tree` just calls
163+
`update-in` to invoke `split-val` on the value of the number at `split-pos`, if there is one.
164+
165+
```clojure
166+
(defn split-pos [tree] (->> (regular-numbers tree)
167+
(keep (fn [[p v]] (when (>= v 10) p)))
168+
first))
169+
170+
(defn split-val [n] (let [a (quot n 2)
171+
b (- n a)]
172+
[a b]))
173+
174+
(defn split-tree [tree]
175+
(when-some [pos (split-pos tree)]
176+
(update-in tree pos split-val)))
177+
```
178+
179+
### Loose ends
180+
181+
We have three more little functions to write before we create `part1`. First, we need to reduce a tree, watching out
182+
because `reduce` is a core CLojure function. In this case, we start with a tree, explode a leaf if we can, or else
183+
split it, and keep repeating that pattern until there are no more changes. We'll use my best friends `iterate` and
184+
`some-fn`, where the latter attempts to either explode or split the previous tree, and `iterate` creates an infinite
185+
sequence of applying one function or the other. Once neither function does anything, the `some-fn` will return a
186+
falsey value. So `reduce-tree` calls `(take-while some?)` to keep only the valid values, and returns `last` to get the
187+
final state.
188+
189+
```clojure
190+
(defn reduce-tree [tree]
191+
(->> tree
192+
(iterate (partial (some-fn explode-tree split-tree)))
193+
(take-while some?)
194+
last))
195+
```
196+
197+
The `add-trees` function is very simple - given a sequence of trees, it calls a simple (core) `reduce` on it. The code
198+
creates a vector around each pair of trees, and invokes `reduce-tree` to simplify it before moving on to the next tree.
199+
200+
```clojure
201+
(defn add-trees [trees]
202+
(reduce #(reduce-tree (vector %1 %2)) trees))
203+
```
204+
205+
Finally, the `magnitude` of a tree comes from recursively adding the triple of the first value in a node with the
206+
double of the second value. We'll use `mapv` to map the two sides of the tree to either its own value, if it's a
207+
number, or the `magnitude` of its side. Then we again use `mapv` in `(mapv * [3 2] pair-of-values)` to multiple the
208+
two values, and then we'll add them together.
209+
210+
```clojure
211+
(defn magnitude [tree]
212+
(->> (mapv #(if (number? %) % (magnitude %)) tree)
213+
(mapv * [3 2])
214+
(apply +)))
215+
```
216+
217+
Alright, it's time for part 1! Parse the input, add all of the trees together, and calculate the magnitude.
218+
219+
```clojure
220+
(defn part1 [input]
221+
(->> input parse-trees add-trees magnitude))
222+
```
223+
224+
---
225+
226+
## Part 2
227+
228+
Well now, there's almost nothing to do here. We need to combine every combination of trees from the input, recognizing
229+
that snailfish addition is not commutative, since `[[1 2] [2 3]]` does not equal `[[2 3] [1 2]]`. So we'll use a
230+
`for` macro to combine every combination of `t0` and `t1` where they aren't the same. then for each pair of trees,
231+
we'll call `add-trees` and `magnitude`, and then collect the max value to finish the puzzle!
232+
233+
```clojure
234+
(defn part2 [input]
235+
(let [trees (parse-trees input)]
236+
(->> (for [t0 trees, t1 trees, :when (not= t0 t1)] [t0 t1])
237+
(map (comp magnitude add-trees))
238+
(apply max))))
239+
```

resources/day18_data.txt

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
[3,[5,[7,[3,9]]]]
2+
[[[[7,0],0],[2,[2,8]]],[[[7,8],1],3]]
3+
[[[[2,7],0],7],4]
4+
[[2,1],[9,0]]
5+
[[[[7,1],[3,2]],[[9,8],5]],[2,7]]
6+
[[[8,9],[[8,7],0]],[[[8,7],[6,3]],[[1,7],[8,9]]]]
7+
[[8,6],[[9,[1,7]],[6,[3,9]]]]
8+
[[2,[[5,6],6]],[[4,[5,9]],[3,[4,5]]]]
9+
[[[[2,0],[1,1]],[6,6]],[[1,9],[[2,7],[6,8]]]]
10+
[[[4,6],[[6,3],[3,9]]],[[[2,6],[6,1]],[[9,9],[1,5]]]]
11+
[[[4,[3,1]],3],6]
12+
[[0,[[5,2],8]],[1,[9,[4,3]]]]
13+
[[[[8,6],[2,1]],[2,[8,6]]],[[[7,1],[3,9]],0]]
14+
[[[[4,7],[2,7]],[[8,9],2]],[[[2,4],[7,2]],[3,7]]]
15+
[[5,[2,2]],[[1,6],[[9,1],[5,0]]]]
16+
[[5,[[1,2],[6,4]]],[6,8]]
17+
[[[5,[1,7]],7],[7,[8,1]]]
18+
[[1,9],[[0,3],[[6,7],[2,4]]]]
19+
[1,[7,[[0,6],0]]]
20+
[[[[5,7],9],[[3,2],7]],[[5,1],[9,9]]]
21+
[[[[0,4],[9,6]],[[8,3],[7,4]]],[7,[6,2]]]
22+
[[[[1,6],0],[[8,0],[3,4]]],[[3,[0,3]],4]]
23+
[4,[[7,8],[4,[9,7]]]]
24+
[[[2,[3,7]],5],[0,[9,9]]]
25+
[[[2,0],[[5,8],[7,6]]],[[9,[6,2]],[3,2]]]
26+
[[[3,1],3],[[[3,7],6],[9,8]]]
27+
[[7,[[2,5],5]],[5,[3,[4,5]]]]
28+
[[[6,7],6],[2,[[9,3],9]]]
29+
[[[[5,6],7],[[3,2],5]],[[9,[4,3]],[3,8]]]
30+
[0,7]
31+
[[[4,6],[2,9]],[[[7,6],[5,1]],7]]
32+
[[0,5],[[1,[4,1]],[[7,3],9]]]
33+
[[[2,[3,8]],5],[[[5,9],8],[7,0]]]
34+
[[[6,[8,6]],[[3,6],7]],[[2,1],[6,[7,5]]]]
35+
[[2,[[6,3],[8,9]]],[[[5,6],4],[[7,0],1]]]
36+
[[[[7,1],[5,6]],8],[[[8,9],4],[8,3]]]
37+
[[[9,2],[1,0]],0]
38+
[[5,[5,[8,5]]],4]
39+
[[3,[5,[4,9]]],3]
40+
[[8,[[7,7],6]],5]
41+
[[4,[[5,1],1]],[1,[1,[9,8]]]]
42+
[[[7,[3,6]],[[2,8],[4,7]]],[[[8,8],[4,0]],[2,4]]]
43+
[[[[3,6],3],[0,9]],2]
44+
[[2,8],[[8,[8,6]],[[1,1],[4,5]]]]
45+
[[2,[1,[1,0]]],[[[6,2],[7,4]],[[7,1],6]]]
46+
[3,[8,[7,[8,6]]]]
47+
[[1,0],[[[0,4],[0,5]],[1,5]]]
48+
[[[[5,0],4],[[7,8],[8,8]]],[[1,7],0]]
49+
[1,[[[4,1],7],[6,[9,0]]]]
50+
[[[1,8],2],[[5,5],[8,5]]]
51+
[[4,[9,[0,6]]],[[[8,9],[4,5]],4]]
52+
[[[[5,4],[1,7]],[[3,1],[7,9]]],[[[0,8],[4,7]],[[5,9],6]]]
53+
[[[[8,0],9],4],[[7,[1,3]],5]]
54+
[[[[5,0],6],[[6,1],8]],[[9,1],7]]
55+
[[9,[6,[8,8]]],[7,[[7,1],6]]]
56+
[[[5,[1,5]],[3,[4,2]]],[[[5,2],7],[[6,9],[2,8]]]]
57+
[[[5,[5,5]],[5,7]],[4,[[2,9],7]]]
58+
[[[[0,4],0],[[0,6],[3,0]]],[0,[[8,1],2]]]
59+
[[[7,[4,6]],[[7,2],[4,6]]],[[[9,3],[4,9]],6]]
60+
[[6,7],7]
61+
[[[4,1],[8,[1,5]]],[[4,6],0]]
62+
[[[4,[5,5]],5],[[0,[2,7]],[1,1]]]
63+
[[[[0,1],3],[6,7]],[4,7]]
64+
[[4,[6,4]],[[[9,8],1],[9,3]]]
65+
[[[4,9],0],[[[7,0],[0,9]],[1,[1,0]]]]
66+
[[[7,9],[[9,5],[6,9]]],[[0,[3,0]],[0,[5,9]]]]
67+
[9,[[0,0],[[1,9],9]]]
68+
[[[5,[0,5]],[[9,8],[9,5]]],[[0,[2,5]],7]]
69+
[[[[5,8],6],9],[[[2,7],7],[[7,8],5]]]
70+
[[8,[[4,7],6]],2]
71+
[[[[7,1],[9,0]],[9,[1,7]]],[[8,[6,7]],[2,5]]]
72+
[[4,[2,9]],8]
73+
[[[[7,6],[5,3]],[5,[9,7]]],[[6,[8,1]],[[6,4],9]]]
74+
[[7,[[7,8],4]],[[1,3],[4,[9,7]]]]
75+
[[[6,[6,7]],[[2,8],3]],[7,[6,[0,3]]]]
76+
[[9,8],[[0,[4,8]],[[9,1],1]]]
77+
[[[[4,0],[5,9]],7],[6,[[5,9],[9,6]]]]
78+
[[8,1],[1,[9,[8,3]]]]
79+
[[[1,[5,1]],[6,7]],[[5,9],[2,[6,7]]]]
80+
[[[3,7],[[7,8],1]],[[0,[6,3]],[8,0]]]
81+
[[5,[[9,3],[1,2]]],7]
82+
[[[1,[9,9]],3],[[6,4],[4,1]]]
83+
[[6,[1,[3,6]]],[2,9]]
84+
[[2,[0,2]],[5,[[9,4],[5,0]]]]
85+
[[4,[[3,1],[7,0]]],[[9,1],[[5,5],[6,7]]]]
86+
[[3,[[7,1],[3,4]]],[7,[9,[9,4]]]]
87+
[[9,9],[[5,4],[[9,7],4]]]
88+
[[[5,1],8],[[6,7],9]]
89+
[[[0,[9,5]],[4,3]],[3,2]]
90+
[[[6,[4,1]],[[8,7],[5,3]]],[[[1,2],5],[[9,2],5]]]
91+
[[[[7,4],[9,0]],[[1,8],[2,9]]],[[5,[1,9]],[4,0]]]
92+
[[[4,[3,8]],[[3,3],[2,8]]],[[[1,3],9],[[8,5],6]]]
93+
[[[[6,4],[7,9]],[[7,6],8]],[7,[9,8]]]
94+
[[7,[3,5]],7]
95+
[[[[5,0],[2,3]],[3,7]],[[4,[6,3]],[7,[4,4]]]]
96+
[[6,[3,[7,6]]],[[[5,8],[8,1]],[3,[1,5]]]]
97+
[[8,[9,[5,2]]],2]
98+
[[1,[5,4]],[[7,[8,0]],8]]
99+
[[[[2,7],4],3],[[1,4],[8,4]]]
100+
[3,[9,2]]

0 commit comments

Comments
 (0)