Skip to content

Commit ca39870

Browse files
committed
Day 6
1 parent ee63e98 commit ca39870

File tree

5 files changed

+167
-0
lines changed

5 files changed

+167
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
| 3 | [source](src/advent_2021_clojure/day03.clj) | [blog](docs/day03.md) |
88
| 4 | [source](src/advent_2021_clojure/day04.clj) | [blog](docs/day04.md) |
99
| 5 | [source](src/advent_2021_clojure/day05.clj) | [blog](docs/day05.md) |
10+
| 6 | [source](src/advent_2021_clojure/day06.clj) | [blog](docs/day06.md) |

docs/day06.md

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Day Six: Lanternfish
2+
3+
* [Problem statement](https://adventofcode.com/2021/day/6)
4+
* [Solution code](https://github.com/abyala/advent-2021-clojure/blob/master/src/advent_2021_clojure/day06.clj)
5+
6+
---
7+
8+
## Preamble
9+
10+
This might be one of the simplest puzzles I've seen in a while. My write-up breaks apart more succinct code into
11+
more pieces so it's easier to read, even though this doubles the number of lines actually needed. Anyway, off we go!
12+
13+
---
14+
15+
## Part 1
16+
17+
We are observing lanternfish having babies. How awkward. Every fish has a timer, and on its zeroth day, it resets its
18+
timer to 6 and creates a baby lanternfish with a timer of 8. It's the miracle of digital life.
19+
20+
Now the puzzle does each and every fish individually, what its timer is, and the emergence of new fish with timers of
21+
8. I decided not to represent every fish individually, but instead just record how many fish have a certain timer on
22+
9. each day. It turns out I wasn't punished for this when I got to part 2, so score!
23+
24+
My target data representation is a simple map of `{timer fish-count}`, given a single-line input string with
25+
comma-separated starting timers. The easiest approach is to use `re-seq` to do a reg-ex extraction of every number
26+
between the commas, map it to an integer using `parse-int`, and then call the `frequencies` function. This core
27+
function takes in a sequence and returns a map of every value to its count within the sequence, which conveniently is
28+
exactly what we need here! To make later code cleaner, I'll merge the input fish into a default map of `no-fish`,
29+
where all numbers 0 through 8 are mapped to zero, such that every possible timer value is in the map. It's not strictly
30+
necessary, but it helps.
31+
32+
```clojure
33+
(def no-fish {0 0, 1 0, 2 0, 3 0, 4 0, 5 0, 6 0, 7 0, 8 0})
34+
35+
(defn parse-fish [input]
36+
(->> (re-seq #"\d" input)
37+
(map parse-int)
38+
frequencies
39+
(merge no-fish)))
40+
```
41+
42+
Now the whole problem statement really comes down to one function - `next-generation`, but we've got a little setup to
43+
make it simple to read. First, I define convenience constants of `delivery-timer`, `post-delivery-timer` and
44+
`new-fish-timer`, set to 0, 6 and 8, respectively; the zero may seem like overkill, but I want my code to tell a
45+
story. Then I make a helper function called `next-timer` that defines the next generation of a timer. Values of 0 (aka
46+
`delivery-timer`) become 6 (aka `post-delivery-timer`), and everything else decrements.
47+
48+
```clojure
49+
(def delivery-timer 0)
50+
(def post-delivery-timer 6)
51+
(def new-fish-timer 8)
52+
53+
(defn next-timer [timer]
54+
(if (zero? timer) post-delivery-timer (dec timer)))
55+
```
56+
57+
Now we can build `next-generation` cleanly in two steps. First, starting with the `no-fish` map again, we map every
58+
timer to its `next-timer` value, and add it to the map. Then we add in the new fish. Now the question is, why do we
59+
_add_ the fish values to the map instead of just setting them? It's because the next generation's number of fish with
60+
a timer of 6 is the sum of the previous generation's fish with timers of 0 and 7.
61+
62+
One terrific function I want to show off here is `reduce-kv`, which is another form of `reduce`. Both take in
63+
3 arguments - a reducing function, an initial value (this is actually optional for `reduce), and the collection to
64+
reduce over. However, while `reduce` has a 2-arity function of `[accumulator value]`, `reduce-kv` requires the
65+
collection to be associative, and thus its reducing function is `3-arity` or form `[accumulator key value]`. This is
66+
effectively the same as using `(reduce (fn [accumulator [key value]] ...))`, but it's clearer to see what's going on
67+
by using `reduce-kv`.
68+
69+
```clojure
70+
(defn next-generation [fish]
71+
(let [deliveries (fish delivery-timer)]
72+
(-> (reduce-kv (fn [m k v] (update m (next-timer k) + v)) no-fish fish)
73+
(assoc new-fish-timer deliveries))))
74+
```
75+
76+
Almost done. Before creating the `solve` function, let's define `nth-generation`, a function which takes in the day
77+
number we want to look at, as well as the initial map of fish, and then return the map on that day. We'll just use
78+
`iterate` invoking `next-generation`, and then use `nth` to get the correct resulting value.
79+
80+
```clojure
81+
(defn nth-generation [n fish]
82+
(-> (iterate next-generation fish)
83+
(nth n)))
84+
```
85+
86+
Finally, we create `solve`, which takes in both the input and the number of days. The function parses the fish, invokes
87+
`nth-generation` to get the state on the last day, calls `vals` to get to the number of fish, and then `(apply +)` to
88+
add them up. And the `day1` function just calls `solve` with 80 for the number of days.
89+
90+
```clojure
91+
(defn solve [input num-days]
92+
(->> (parse-fish input)
93+
(nth-generation num-days)
94+
vals
95+
(apply +)))
96+
97+
(defn day1 [input] (solve input 80))
98+
```
99+
100+
---
101+
102+
## Part 2
103+
104+
Well now we just need to get the 256th day instead of the 80th day. I'm assuming this just asserts that our algorithms
105+
don't contain a giant list of each and every fish, because my data set brought me into the trillians of fish. Our
106+
algorithm can handle the extra data, no problem. One nice thing about Clojure is that it will automatically convert
107+
Ints into Longs, or Floats into Doubles, to avoid overflow or underflow.
108+
109+
Anyway, here's the one-line `day2` function.
110+
111+
```clojure
112+
(defn day2 [input] (solve input 256))
113+
```
114+
115+
Be free, lanternfish, and procreate until we run out of water molecules in the sea!

resources/day06_data.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2,3,1,3,4,4,1,5,2,3,1,1,4,5,5,3,5,5,4,1,2,1,1,1,1,1,1,4,1,1,1,4,1,3,1,4,1,1,4,1,3,4,5,1,1,5,3,4,3,4,1,5,1,3,1,1,1,3,5,3,2,3,1,5,2,2,1,1,4,1,1,2,2,2,2,3,2,1,2,5,4,1,1,1,5,5,3,1,3,2,2,2,5,1,5,2,4,1,1,3,3,5,2,3,1,2,1,5,1,4,3,5,2,1,5,3,4,4,5,3,1,2,4,3,4,1,3,1,1,2,5,4,3,5,3,2,1,4,1,4,4,2,3,1,1,2,1,1,3,3,3,1,1,2,2,1,1,1,5,1,5,1,4,5,1,5,2,4,3,1,1,3,2,2,1,4,3,1,1,1,3,3,3,4,5,2,3,3,1,3,1,4,1,1,1,2,5,1,4,1,2,4,5,4,1,5,1,5,5,1,5,5,2,5,5,1,4,5,1,1,3,2,5,5,5,4,3,2,5,4,1,1,2,4,4,1,1,1,3,2,1,1,2,1,2,2,3,4,5,4,1,4,5,1,1,5,5,1,4,1,4,4,1,5,3,1,4,3,5,3,1,3,1,4,2,4,5,1,4,1,2,4,1,2,5,1,1,5,1,1,3,1,1,2,3,4,2,4,3,1

src/advent_2021_clojure/day06.clj

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
(ns advent-2021-clojure.day06
2+
(:require [advent-2021-clojure.utils :refer [parse-int]]))
3+
4+
(def delivery-timer 0)
5+
(def post-delivery-timer 6)
6+
(def new-fish-timer 8)
7+
(def no-fish {0 0, 1 0, 2 0, 3 0, 4 0, 5 0, 6 0, 7 0, 8 0})
8+
9+
(defn parse-fish [input]
10+
(->> (re-seq #"\d" input)
11+
(map parse-int)
12+
frequencies
13+
(merge no-fish)))
14+
15+
(defn next-timer [timer]
16+
(if (= timer delivery-timer) post-delivery-timer (dec timer)))
17+
18+
(defn next-generation [fish]
19+
(let [deliveries (fish delivery-timer)]
20+
(-> (reduce-kv (fn [m k v] (update m (next-timer k) + v)) no-fish fish)
21+
(assoc new-fish-timer deliveries))))
22+
23+
(defn nth-generation [n fish]
24+
(-> (iterate next-generation fish)
25+
(nth n)))
26+
27+
(defn solve [input num-days]
28+
(->> (parse-fish input)
29+
(nth-generation num-days)
30+
vals
31+
(apply +)))
32+
33+
(defn day1 [input] (solve input 80))
34+
(defn day2 [input] (solve input 256))
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
(ns advent-2021-clojure.day06-test
2+
(:require [clojure.test :refer :all]
3+
[advent-2021-clojure.day06 :refer :all]))
4+
5+
(def test-input "3,4,3,1,2")
6+
(def puzzle-input (slurp "resources/day06_data.txt"))
7+
8+
(deftest part1-test
9+
(are [expected input] (= expected (day1 input))
10+
5934 test-input
11+
358214 puzzle-input))
12+
13+
(deftest part2-test
14+
(are [expected input] (= expected (day2 input))
15+
26984457539 test-input
16+
1622533344325 puzzle-input))

0 commit comments

Comments
 (0)