|
| 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 | +``` |
0 commit comments