Skip to content

Commit c97df7e

Browse files
committed
Day 12
1 parent 56d79f1 commit c97df7e

File tree

5 files changed

+268
-0
lines changed

5 files changed

+268
-0
lines changed

docs/day12.md

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Day Twelve: Passage Pathing
2+
3+
* [Problem statement](https://adventofcode.com/2021/day/12)
4+
* [Solution code](https://github.com/abyala/advent-2021-clojure/blob/master/src/advent_2021_clojure/day12.clj)
5+
6+
---
7+
8+
## Preamble
9+
10+
In today's puzzle, I decided to use, or rather overuse, some functions I've only played with a bit in the past, namely
11+
`some-fn` and `every-pred`. I freely admit that I sacrificed some readability for the sake of trying a new approach,
12+
so even though the code is clear enough, I'm not sure I necessarily like it. Today's solution was a fun experiment!
13+
14+
---
15+
16+
## Part 1
17+
18+
We're navigating through underwater caves, looking for every possible path from the start to the end, such that we
19+
never visit the same small cave twice. A small cave has an all-lowercase name, while a big cave has an all-uppercase
20+
name. Both `"start"` and `"end"` are special cases, where we never return to the start, and we're done as soon as we
21+
reach the end.
22+
23+
The data comes in as a sequence of lines of cave names, separated by a dash. We observe that if you can navigate from
24+
cave A to B, then you can also go from B to A, so we'll want to create a map from every cave to every cave it connects
25+
to. Since we never want to return to the start, we'll explicitly remove any such mapping once we're done constructing
26+
the original map; we do this at the end so that we don't have to check both the left- and right-hand-side of each line.
27+
Since "start" and "end" are magic strings we'll refer to several times, we'll define them as vars right away.
28+
29+
```clojure
30+
(def start-cave "start")
31+
(def end-cave "end")
32+
33+
(defn parse-connections [input]
34+
(let [every-path (->> (str/split-lines input)
35+
(map #(str/split % #"-"))
36+
(reduce (fn [acc [from to]] (-> acc
37+
(update from conj to)
38+
(update to conj from))) {}))]
39+
(utils/update-values every-path #(remove (partial = start-cave) %))))
40+
```
41+
42+
Since we're talking about simple vars, let's define a few other helper functions so we can remove some lambdas later
43+
on. And to do this, we'll revisit our old friend `some-fn` and introduce a new one named `every-pred`. Recall from
44+
days 5 and 9 that we use `(some-fn pred1 pred2... predn)` to take in a number of predicates, and return a new one that
45+
returns the first predicate which returns a truthy value. It's our way of saying "do any of these predicates pass?"
46+
We'll now use a similar function called `every-pred`, which has a similar constructions and returns a boolean value
47+
for whether every predicate returns a truthy value. Essentially, `some-fn` is an aggregate `or`, while `every-pred` is
48+
an aggregate `and`. Why isn't the first named `some-pred`, or the second named `every-fn`? And more relevant here,
49+
where isn't there a `no-pred` or `not-any-fn` to aggregate `not`?
50+
51+
Anyway, we'll use `start-cave?` and `end-cave?` to look for those special values; anything that isn't a start or end
52+
cave is an intermediate cave, so we'll use the `complement` (opposite) of `some-fn` to create `intermediate-cave?`.
53+
Finally, `small-cave?` and `big-cave?` are just intermediate caves whose string values are all lowercase or uppercase.
54+
I made two helper functions here in the `utils` package, which are the String equivalent of `Character/isUpperCase`
55+
and `Character/isLowerCase`.
56+
57+
```clojure
58+
; advent-2021-clojure.utils namespace
59+
(defn lower-case? [s] (every? #(Character/isLowerCase ^char %) s))
60+
(defn upper-case? [s] (every? #(Character/isUpperCase ^char %) s))
61+
62+
; advent-2021-clojure.day12 namespace
63+
(defn start-cave? [cave] (= start-cave cave))
64+
(defn end-cave? [cave] (= end-cave cave))
65+
(defn intermediate-cave? [cave] ((complement (some-fn start-cave? end-cave?)) cave))
66+
(defn small-cave? [cave] ((every-pred intermediate-cave? utils/lower-case?) cave))
67+
(defn big-cave? [cave] ((every-pred intermediate-cave? utils/upper-case?) cave))
68+
```
69+
70+
Let's think about the `find-paths` function we're about to make. It will need to take in the `connections` map, and
71+
should return all paths we can take from the start to the end, as a sequence of path vectors. We don't explicitly need
72+
the paths themselves, but it seems intuitive to structure it that way. We'll make a simple recursive function,
73+
and we'll provide both a 1-arity implementation (just the `connections`) and a 3-arity implementation (`connections`,
74+
the path to the current cave, and the set of caves we've already seen). We'll look at the last point in the path, and
75+
check to see if it's the end; if so, we return a single-element vector with the current path, since we made it to the
76+
end. If not, we'll look at all connections out from that cave, filter out for the ones that are approachable
77+
(to be implemented later), and then mapcat the recursive call from each approachable cave onto the path ending with
78+
that cave. We'll also add the latest cave to the set of `seen` caves, so we know where we've already been.
79+
80+
```clojure
81+
(defn find-paths
82+
([connections]
83+
(find-paths connections [start-cave] #{}))
84+
85+
([connections path seen]
86+
(let [cave (last path)]
87+
(if (end-cave? cave)
88+
[path]
89+
(->> (connections cave)
90+
(filter (partial approachable? seen))
91+
(mapcat (fn [cave]
92+
(find-paths connections (conj path cave) (conj seen cave)))))))))
93+
```
94+
95+
So what makes a cave approachable? There are three reasons you can go into a cave:
96+
1. This is the end cave, because that finishes the path.
97+
2. This is a big cave, because we can visit big caves multiple times.
98+
3. This is a cave we haven't seen yet, suggesting it's a small cave.
99+
100+
To implement `approachable?`, we'll use `some-fn` to check if any of those predicates apply to the cave.
101+
102+
```clojure
103+
(defn approachable11 [seen cave]
104+
((some-fn end-cave? big-cave? (complement seen)) cave))
105+
```
106+
107+
Finally, we can implement `part1` by parsing the input, finding all paths from the start to the end, and then counting
108+
the number of paths returned.
109+
110+
```clojure
111+
(defn part1 [input] (->> input parse-connections find-paths count))
112+
```
113+
114+
---
115+
116+
## Part 2
117+
118+
In part 2, we learn that we can still visit big caves any number of times, but we can also revisit a single small cave
119+
once. We'll have to make a small modification to the existing functions to get this to work, but overall our structure
120+
should hold.
121+
122+
I think the easiest way to think about this is to go back to `approachable?` before rewriting `find-paths`. The
123+
`approachable?` function can now allow entry into a small cave if there hasn't already been a double-entry cave.
124+
So now, the `approachable?` function takes in three arguments - an `allow-repeat-visit?` flag that's false for part 1
125+
and true for part 2, a _map_ (not set) of caves already seen to the number of visits, and the cave in question. The
126+
`((some-fn end-cave? big-cave? (complement seen)) cave)` code from Part 1 still works just fine. But now, if none of
127+
those predicates apply to the cave, we can allow the cave if both `allow-repeat-visit?` is true and we have not seen
128+
any repeat visitors. The `has-repeats?` function will check if any cave has been visited at least twice already.
129+
130+
```clojure
131+
(defn has-repeats? [seen] (some (partial <= 2) (vals seen)))
132+
133+
(defn approachable? [allow-repeat-visit? seen cave]
134+
(or ((some-fn end-cave? big-cave? (complement seen)) cave)
135+
(and allow-repeat-visit? (not (has-repeats? seen)))))
136+
```
137+
138+
Now that that works, we need to make some small changes to `find-paths`. First of all, its arity will switch to a
139+
2-arity `(allow-repeat-visit?` and `connections`) and a 4-arity (`allow-repeat-visit?`, `connections`, the `path`, and
140+
the set of only the small caves we've already seen). Most of the logic is the same, other than passing in an empty map
141+
instead of an empty set to the 4-arity implementation. But when we're ready to recursively call `find-paths`, we need
142+
to only increment the mapping of the current `cave` if it's small. Unfortunately, `(update {} :a inc)` will throw a
143+
`NullPointerException` because you can't increment a value that's not in the map yet, so to accommodate for the first
144+
instances of visiting a small cave, we'll need to execute `(update small-caves-seen #(inc (or % 0)))`; the `or`
145+
function is a great way to handle nulls, since `#(inc (or % 0))` means we increment the current value in the map, or
146+
0 if it's not in the map.
147+
148+
```clojure
149+
(defn find-paths
150+
([allow-repeat-visit? connections]
151+
(find-paths allow-repeat-visit? connections [start-cave] {}))
152+
153+
([allow-repeat-visit? connections path small-caves-seen]
154+
(let [cave (last path)]
155+
(if (end-cave? cave)
156+
[path]
157+
(->> (connections cave)
158+
(filter (partial approachable? allow-repeat-visit? small-caves-seen))
159+
(mapcat (fn [cave]
160+
(find-paths allow-repeat-visit?
161+
connections
162+
(conj path cave)
163+
(if (small-cave? cave)
164+
(update small-caves-seen cave #(inc (or % 0)))
165+
small-caves-seen)))))))))
166+
```
167+
168+
Finally, we can refactor `part1` and implement `part2` by calling the new `find-paths` function, passing in the
169+
appropriate values for `allow-repeat-visit?`.
170+
171+
```clojure
172+
(defn part1 [input] (->> input parse-connections (find-paths false) count))
173+
(defn part2 [input] (->> input parse-connections (find-paths true) count))
174+
```

resources/day12_data.txt

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
KF-sr
2+
OO-vy
3+
start-FP
4+
FP-end
5+
vy-mi
6+
vy-KF
7+
vy-na
8+
start-sr
9+
FP-lh
10+
sr-FP
11+
na-FP
12+
end-KF
13+
na-mi
14+
lh-KF
15+
end-lh
16+
na-start
17+
wp-KF
18+
mi-KF
19+
vy-sr
20+
vy-lh
21+
sr-mi

src/advent_2021_clojure/day12.clj

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
(ns advent-2021-clojure.day12
2+
(:require
3+
[advent-2021-clojure.utils :as utils]
4+
[clojure.string :as str]))
5+
6+
(def start-cave "start")
7+
(def end-cave "end")
8+
9+
(defn parse-connections [input]
10+
(let [every-path (->> (str/split-lines input)
11+
(map #(str/split % #"-"))
12+
(reduce (fn [acc [from to]] (-> acc
13+
(update from conj to)
14+
(update to conj from))) {}))]
15+
(utils/update-values every-path #(remove (partial = start-cave) %))))
16+
17+
(defn start-cave? [cave] (= start-cave cave))
18+
(defn end-cave? [cave] (= end-cave cave))
19+
(defn intermediate-cave? [cave] ((complement (some-fn start-cave? end-cave?)) cave))
20+
(defn small-cave? [cave] ((every-pred intermediate-cave? utils/lower-case?) cave))
21+
(defn big-cave? [cave] ((every-pred intermediate-cave? utils/upper-case?) cave))
22+
23+
(defn has-repeats? [seen] (some (partial <= 2) (vals seen)))
24+
25+
(defn approachable? [allow-repeat-visit? seen cave]
26+
(or ((some-fn end-cave? big-cave? (complement seen)) cave)
27+
(and allow-repeat-visit? (not (has-repeats? seen)))))
28+
29+
(defn find-paths
30+
([allow-repeat-visit? connections]
31+
(find-paths allow-repeat-visit? connections [start-cave] {}))
32+
33+
([allow-repeat-visit? connections path small-caves-seen]
34+
(let [cave (last path)]
35+
(if (end-cave? cave)
36+
[path]
37+
(->> (connections cave)
38+
(filter (partial approachable? allow-repeat-visit? small-caves-seen))
39+
(mapcat (fn [cave]
40+
(find-paths allow-repeat-visit?
41+
connections
42+
(conj path cave)
43+
(if (small-cave? cave)
44+
(update small-caves-seen cave #(inc (or % 0)))
45+
small-caves-seen)))))))))
46+
47+
(defn part1 [input] (->> input parse-connections (find-paths false) count))
48+
(defn part2 [input] (->> input parse-connections (find-paths true) count))

src/advent_2021_clojure/utils.clj

+3
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@
2626
http://blog.jayfields.com/2011/08/clojure-apply-function-to-each-value-of.html"
2727
[m f & args]
2828
(reduce (fn [r [k v]] (assoc r k (apply f v args))) {} m))
29+
30+
(defn lower-case? [s] (every? #(Character/isLowerCase ^char %) s))
31+
(defn upper-case? [s] (every? #(Character/isUpperCase ^char %) s))
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
(ns advent-2021-clojure.day12-test
2+
(:require [clojure.test :refer :all]
3+
[advent-2021-clojure.day12 :refer :all]))
4+
5+
(def simple-test "start-A\nstart-b\nA-c\nA-b\nb-d\nA-end\nb-end")
6+
(def test-input "dc-end\nHN-start\nstart-kj\ndc-start\ndc-HN\nLN-dc\nHN-end\nkj-sa\nkj-HN\nkj-dc")
7+
(def large-test "fs-end\nhe-DX\nfs-he\nstart-DX\npj-DX\nend-zg\nzg-sl\nzg-pj\npj-he\nRW-he\nfs-DX\npj-RW\nzg-RW\nstart-pj\nhe-WI\nzg-he\npj-fs\nstart-RW")
8+
(def puzzle-input (slurp "resources/day12_data.txt"))
9+
10+
(deftest part1-test
11+
(are [expected input] (= expected (part1 input))
12+
10 simple-test
13+
19 test-input
14+
226 large-test
15+
4885 puzzle-input))
16+
17+
(deftest part2-test
18+
(are [expected input] (= expected (part2 input))
19+
36 simple-test
20+
103 test-input
21+
3509 large-test
22+
117095 puzzle-input))

0 commit comments

Comments
 (0)