|
| 1 | +^{:nextjournal.clerk/visibility {:code :hide :result :hide}} |
| 2 | +(ns day11 |
| 3 | + {:title "Reactor" |
| 4 | + :url "https://adventofcode.com/2025/day/11" |
| 5 | + :extras "" |
| 6 | + :highlights "" |
| 7 | + :remark "Surprisingly easy." |
| 8 | + :nextjournal.clerk/auto-expand-results? true |
| 9 | + :nextjournal.clerk/toc true} |
| 10 | + (:require [aoc-utils.core :as aoc])) |
| 11 | + |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +;; # Day 11: [Reactor](https://adventofcode.com/2025/day/11) |
| 16 | +;; |
| 17 | +;; The Elves have installed a new server rack and now need our help with |
| 18 | +;; connecting some devices with cables. |
| 19 | +;; They give us a list of connections between the devices which looks like |
| 20 | +;; this: |
| 21 | + |
| 22 | +(def example "aaa: you hhh |
| 23 | +you: bbb ccc |
| 24 | +bbb: ddd eee |
| 25 | +ccc: ddd eee fff |
| 26 | +ddd: ggg |
| 27 | +eee: out |
| 28 | +fff: out |
| 29 | +ggg: out |
| 30 | +hhh: ccc fff iii |
| 31 | +iii: out") |
| 32 | + |
| 33 | +;; Each line tells us how to connect a device's outputs to the correct devices. |
| 34 | +;; For example, `aaa: you hhh` means we need to connect device `aaa` to |
| 35 | +;; `you` and `hhh`. And the connection is directed, there is no `you` to |
| 36 | +;; `aaa` or `hhh` to `aaa` connection. |
| 37 | + |
| 38 | + |
| 39 | + |
| 40 | + |
| 41 | + |
| 42 | +;; ## Input parsing |
| 43 | +;; |
| 44 | +;; We're dealing with an acyclic graph, so let's build it immediately in the |
| 45 | +;; parsing phase. |
| 46 | + |
| 47 | + |
| 48 | +(defn build-graph [lines] |
| 49 | + (reduce (fn [acc [start & ends]] |
| 50 | + (assoc acc start ends)) |
| 51 | + {} |
| 52 | + lines)) |
| 53 | + |
| 54 | +(defn parse-data [input] |
| 55 | + (-> input |
| 56 | + (aoc/parse-lines :keywords #"\W+") ; [1] |
| 57 | + build-graph)) ; [2] |
| 58 | + |
| 59 | +;; For each line in the input, we will split the string on `\W+`, which is |
| 60 | +;; a regex speak for "one or more non-characters". |
| 61 | +;; We will convert each name of a device into a keyword for convenience [1]. |
| 62 | +;; (I've added `:keywords` to the |
| 63 | +;; [aoc-utils library](https://narimiran.github.io/aoc-utils/aoc-utils.core.html) |
| 64 | +;; after I've seen this task.)\ |
| 65 | +;; Since the graph is acyclic, creating a hashmap representation of it is |
| 66 | +;; straightforward [2]. |
| 67 | + |
| 68 | +(def example-data (parse-data example)) |
| 69 | +(def data (parse-data (aoc/read-input 11))) |
| 70 | + |
| 71 | + |
| 72 | + |
| 73 | + |
| 74 | +;; ## Part 1 |
| 75 | +;; |
| 76 | +;; For Part 1 we need to count all paths from `:you` to `:out`. |
| 77 | +;; |
| 78 | +;; For acyclic graphs, when traversing them we don't need to remember what |
| 79 | +;; places we've visited, we just go go go until we reach the end. |
| 80 | +;; |
| 81 | +;; We'll do it recursively. Every time we come to `:out`, we count that as |
| 82 | +;; one successful path. If we come to the end of the graph (no neighbours of a |
| 83 | +;; current point), the path was not successful. If there are some neighbours |
| 84 | +;; of a current point, we try to reach `:out` with all of them. |
| 85 | + |
| 86 | +(def paths |
| 87 | + (memoize ; [1] |
| 88 | + (fn [graph curr end] |
| 89 | + (if (= curr end) 1 ; [2] |
| 90 | + (if-let [nbs (graph curr)] |
| 91 | + (aoc/sum-map #(paths graph % end) nbs) ; [3] |
| 92 | + 0))))) ; [4] |
| 93 | + |
| 94 | +;; If there is a path `A - B - C - D - out` and `H - C - D - out` it means we will |
| 95 | +;; visit points `C` and `D` multiple times. And we already know from the first |
| 96 | +;; visit it will be a successful path. We will use our friend |
| 97 | +;; [`memoize`](https://clojuredocs.org/clojure.core/memoize) that we met |
| 98 | +;; in the [Day 7 solution](../day07). [1]\ |
| 99 | +;; For Part 1 we could ignore this inefficiency of these multiple visits, |
| 100 | +;; but for Part 2 it is crucial to memoize. |
| 101 | +;; |
| 102 | +;; If our `curr`ent point is the `end` we wanted, that counts as one successful |
| 103 | +;; path [2]. Otherwise, if there are neighbours of the current point, for |
| 104 | +;; each of them we recursively call this function and sum up all successful |
| 105 | +;; paths [3]. If we reached the end (no neighbours), it was an unsuccessful |
| 106 | +;; path and we return zero [4]. |
| 107 | +;; |
| 108 | +;; For Part 1 we need to count the paths from `:you` to `:out`: |
| 109 | + |
| 110 | +(defn part-1 [graph] |
| 111 | + (paths graph :you :out)) |
| 112 | + |
| 113 | +(part-1 example-data) |
| 114 | +(part-1 data) |
| 115 | + |
| 116 | + |
| 117 | +;; Easy-peasy! |
| 118 | + |
| 119 | + |
| 120 | + |
| 121 | + |
| 122 | + |
| 123 | + |
| 124 | +;; ## Part 2 |
| 125 | +;; |
| 126 | +;; There is different example for Part 2: |
| 127 | + |
| 128 | +(def example-2 "svr: aaa bbb |
| 129 | +aaa: fft |
| 130 | +fft: ccc |
| 131 | +bbb: tty |
| 132 | +tty: ccc |
| 133 | +ccc: ddd eee |
| 134 | +ddd: hub |
| 135 | +hub: fff |
| 136 | +eee: dac |
| 137 | +dac: fff |
| 138 | +fff: ggg hhh |
| 139 | +ggg: out |
| 140 | +hhh: out") |
| 141 | + |
| 142 | +(def example-data-2 (parse-data example-2)) |
| 143 | + |
| 144 | +;; Now, our task is to find the number of paths starting from `:svr`, |
| 145 | +;; passing through both `:dac` and `:fft`, and ending with `:out`. |
| 146 | +;; |
| 147 | +;; As expected, the search space is huge. Memoizing will be important, but it |
| 148 | +;; is not enough. In a true recursive fashion, let's break the problem down |
| 149 | +;; into simpler problems. |
| 150 | +;; |
| 151 | +;; If our task is to count the ways we can go from point `A` to point `C` via |
| 152 | +;; point `B`, we can split it in counting the ways from `A` to `B` and then |
| 153 | +;; the ways from `B` to `C`. The final result is achieved by multiplying |
| 154 | +;; these two intermediate results. |
| 155 | +;; |
| 156 | +;; There is another thing to notice. Since our graph is acyclic, this means |
| 157 | +;; there is _either_ a connection from `:dac` to `:fft` or a connection |
| 158 | +;; from `:fft` to `:dac`, it can't be both.\ |
| 159 | +;; So we will first check which of these two is a valid connection, and |
| 160 | +;; depending on that we will calculate the remaining paths. |
| 161 | + |
| 162 | + |
| 163 | +(defn part-2 [graph] |
| 164 | + (let [fft-dac (paths graph :fft :dac)] ; [1] |
| 165 | + (if (zero? fft-dac) ; [2] |
| 166 | + (* (paths graph :svr :dac) |
| 167 | + (paths graph :dac :fft) |
| 168 | + (paths graph :fft :out)) |
| 169 | + (* (paths graph :svr :fft) |
| 170 | + fft-dac |
| 171 | + (paths graph :dac :out))))) |
| 172 | + |
| 173 | +;; As stated above, we first calculate the number of paths from `:fft` |
| 174 | +;; to `:dac` [1]. |
| 175 | +;; If there are zero paths [2], then we know we need to do |
| 176 | +;; `svr -> dac -> fft -> out`. |
| 177 | +;; Otherwise, we correctly guessed the correct direction and we calulate |
| 178 | +;; the remaining steps of the `svr -> fft -> dac -> out`. |
| 179 | + |
| 180 | +(part-2 example-data-2) |
| 181 | +(part-2 data) |
| 182 | + |
| 183 | + |
| 184 | + |
| 185 | + |
| 186 | + |
| 187 | +;; ## Conclusion |
| 188 | +;; |
| 189 | +;; This one was a surprisingly easy and straightforward task, escpecially for |
| 190 | +;; the next to last task this year. But I guess we deserved some breathing |
| 191 | +;; room after yesterday's task. |
| 192 | +;; |
| 193 | +;; No new functions to highlight today. |
| 194 | + |
| 195 | + |
| 196 | +;; ---- |
| 197 | +;; |
| 198 | +;; [< Previous solution](../day10) |
| 199 | +;; | [Source code](https://github.com/narimiran/aoc2025/blob/main/clojure/src/day11.clj) |
| 200 | +;; | [Next solution >](../day12) |
| 201 | + |
| 202 | + |
| 203 | + |
| 204 | +^{:nextjournal.clerk/visibility {:code :hide :result :hide}} |
| 205 | +(defn -main [input] |
| 206 | + (let [data (parse-data input)] |
| 207 | + [(part-1 data) |
| 208 | + (part-2 data)])) |
0 commit comments