|
3 | 3 | {:title "Factory" |
4 | 4 | :url "https://adventofcode.com/2025/day/10" |
5 | 5 | :extras "" |
6 | | - :highlights "constantly, cond->, keep" |
| 6 | + :highlights "keep, distinct, juxt, frequencies, group-by" |
7 | 7 | :remark "Divide and conquer." |
8 | 8 | :nextjournal.clerk/auto-expand-results? true |
9 | 9 | :nextjournal.clerk/toc true} |
|
52 | 52 | ;; To convert the `lights` into something we can use later, we remove the first and |
53 | 53 | ;; the last character (`[` and `]`, respectively), and the rest we convert to |
54 | 54 | ;; a vector where `#` gets a `true` value and `.` is `false` [2]. (It's important |
55 | | -;; that it is `false` and not `nil`; can't use set `#{\#}` as a predicate.) |
| 55 | +;; that it is `false` and not nil`; can't use set `#{\#}` as a predicate.) |
56 | 56 | ;; |
57 | 57 | ;; For `joltages`, we just need to extract all integers from the last element [3]. |
58 | 58 | ;; We do the same thing for _each_ `button` [4]. |
|
123 | 123 |
|
124 | 124 |
|
125 | 125 | (defn press-buttons [[goal buttons _]] |
126 | | - (let [initial-lights (mapv (constantly false) goal) ; [1] |
127 | | - initial-state [initial-lights buttons 0]] ; [2] |
128 | | - (loop [[[lights buttons presses] & states'] (list initial-state) |
129 | | - best-result 999999] |
130 | | - (cond |
131 | | - (nil? lights) best-result ; [3] |
132 | | - (= lights goal) (recur states' presses) ; [4] |
133 | | - |
134 | | - (and (seq buttons) ; [5] |
135 | | - (< (inc presses) best-result)) ; [6] |
136 | | - (let [[button & buttons'] buttons |
137 | | - lights' (toggle lights button)] |
138 | | - (recur (conj states' |
139 | | - [lights' buttons' (inc presses)] ; [7] |
140 | | - [lights buttons' presses]) ; [8] |
141 | | - best-result)) |
142 | | - |
143 | | - :else (recur states' best-result))))) ; [9] |
144 | | - |
145 | | - |
146 | | -;; When we start, all lights are off. One way to create a vector of initial |
147 | | -;; lights with the same size as our `goal` is to use the |
148 | | -;; [`constantly` function](https://clojuredocs.org/clojure.core/constantly) |
149 | | -;; which returns a function which will always return the provided argument [1].\ |
150 | | -;; The initial state consists of all lights turned off, all buttons ready |
151 | | -;; to be pressed, and zero button presses so far [2]. |
| 126 | + (loop [[[lights buttons presses] & states'] (list [goal buttons 0]) |
| 127 | + best-result 999999] |
| 128 | + (cond |
| 129 | + (nil? lights) best-result ; [1] |
| 130 | + (every? false? lights) (recur states' presses) ; [2] |
| 131 | + |
| 132 | + (and (seq buttons) ; [3] |
| 133 | + (< (inc presses) best-result)) ; [4] |
| 134 | + (let [[button & buttons'] buttons |
| 135 | + lights' (toggle lights button)] |
| 136 | + (recur (conj states' |
| 137 | + [lights' buttons' (inc presses)] ; [5] |
| 138 | + [lights buttons' presses]) ; [6] |
| 139 | + best-result)) |
| 140 | + |
| 141 | + :else (recur states' best-result)))) ; [7] |
| 142 | + |
| 143 | + |
| 144 | +;; Since going from lights off to lights on is the same as going from lights |
| 145 | +;; on to lights off, we will start with the `goal` and stop when we reach |
| 146 | +;; a state with all lights turned off. |
152 | 147 | ;; |
153 | | -;; We will exit the loop when there are no more states to explore [3]. |
| 148 | +;; We will exit the loop when there are no more states to explore [1]. |
154 | 149 | ;; |
155 | | -;; If the current `lights` match the `goal` we're trying to achieve, we've |
| 150 | +;; If the current `lights` are all off, we've |
156 | 151 | ;; found a new best result (we will see in a minute why this is true), and |
157 | 152 | ;; we continue exploring the rest of the states in attempt to find an even |
158 | | -;; better result [4]. |
| 153 | +;; better result [2]. |
159 | 154 | ;; |
160 | | -;; If there are still `buttons` to press [5] and if there's a chance we |
161 | | -;; could improve the best result so far [6], we have two options: |
162 | | -;; - [7] We press the current `button`, toggling lights to their new state |
| 155 | +;; If there are still `buttons` to press [3] and if there's a chance we |
| 156 | +;; could improve the best result so far [4], we have two options: |
| 157 | +;; - [5] We press the current `button`, toggling lights to their new state |
163 | 158 | ;; (`lights'`). |
164 | | -;; - [8] We skip pressing the current button. The `lights` remain as they |
| 159 | +;; - [6] We skip pressing the current button. The `lights` remain as they |
165 | 160 | ;; were. (Notice the lack of `'`.) |
166 | 161 | ;; |
167 | 162 | ;; We add both those scenarios to the `states'` we wish to explore next with |
168 | 163 | ;; the remaining `buttons'`. |
169 | 164 | ;; |
170 | 165 | ;; Otherwise, we explore the remaining `states'` to see if we can improve |
171 | | -;; the best result [9]. |
| 166 | +;; the best result [7]. |
172 | 167 |
|
173 | 168 | (press-buttons (first example-data)) |
174 | 169 |
|
|
197 | 192 | ;; |
198 | 193 | ;; Thanks to [this brilliant insight](https://old.reddit.com/r/adventofcode/comments/1pk87hl/2025_day_10_part_2_bifurcate_your_way_to_victory/), |
199 | 194 | ;; there is a relatively easy solution for Part 2, which we will implement here.\ |
200 | | -;; I wont't repeat in detail what was said in the linked post, you should |
| 195 | +;; I won't repeat in detail what was said in the linked post, you should |
201 | 196 | ;; definitely read it. |
202 | 197 | ;; |
203 | | -;; For this to work, we need to modify the `press-buttons` function to not |
204 | | -;; just search for the best best result, but to give _all_ button `presses` |
205 | | -;; which would give the `goal` state. |
206 | | - |
207 | | -(def press-buttons' |
208 | | - (memoize |
209 | | - (fn [buttons goal] |
210 | | - (let [initial-lights (mapv (constantly false) goal) |
211 | | - initial-state [initial-lights buttons []]] ; [1] |
212 | | - (loop [[[lights buttons presses] & states'] (list initial-state) |
213 | | - results #{}] ; [2] |
214 | | - (let [results' (cond-> results |
215 | | - (= lights goal) (conj presses))] ; [3] |
216 | | - (cond |
217 | | - (nil? lights) results ; [4] |
218 | | - |
219 | | - (seq buttons) ; [5] |
220 | | - (let [[button & buttons'] buttons |
221 | | - lights' (toggle lights button) |
222 | | - presses' (conj presses button)] |
223 | | - (recur (conj states' |
224 | | - [lights' buttons' presses'] |
225 | | - [lights buttons' presses]) |
226 | | - results')) |
227 | | - |
228 | | - :else (recur states' results')))))))) |
229 | | - |
230 | | - |
231 | | -;; We now track not just the number of button presses, but we need to know |
232 | | -;; exactly what buttons were pressed [1].\ |
233 | | -;; The `results` will be a set of all button-pressing combinations which |
234 | | -;; give us the wanted solution [2]. When we weach the `goal`, we add the |
235 | | -;; current `presses` to the `results` [3]. |
236 | | -;; To conditionally update `results`, only if we reached the `goal`, we can |
237 | | -;; use the [`cond->` macro](https://clojuredocs.org/clojure.core/cond-%3E). |
| 198 | +;; We're not interested anymore in just the best score with the fewest presses |
| 199 | +;; for a goal we want to achieve: for each machine we will calculate _all_ |
| 200 | +;; possible subsets of (not) pressing each button: |
| 201 | + |
| 202 | +(defn all-subsets [buttons] |
| 203 | + (loop [subsets [[]] |
| 204 | + [button & buttons'] buttons] |
| 205 | + (if (nil? button) |
| 206 | + subsets |
| 207 | + (let [subsets' (map #(conj % button) subsets)] |
| 208 | + (recur (into subsets subsets') buttons'))))) |
| 209 | + |
| 210 | +(all-subsets [1 2 3]) |
| 211 | + |
| 212 | + |
| 213 | + |
| 214 | +;; For each button-pressing combination, we want to know two things: |
| 215 | +;; - how much each joltage has changed |
| 216 | +;; - how many buttons we've pressed |
238 | 217 | ;; |
239 | | -;; The exit condition is the same as before: when there are no more states |
240 | | -;; to explore [4].\ |
241 | | -;; The line [5] is the key difference: we're not interested in just the best |
242 | | -;; result (with the fewest button presses), we will continue exploring the |
243 | | -;; states for _every_ button (not) pressed. |
| 218 | +;; To get a vector with those things, we will have to apply two different |
| 219 | +;; functions to the button-combination, and for that we can use |
| 220 | +;; the [`juxt` function](https://clojuredocs.org/clojure.core/juxt). |
| 221 | + |
| 222 | +(def press-results (juxt (comp frequencies flatten) count)) |
| 223 | + |
| 224 | +;; The first result is achieved by [`flatten`ing](https://clojuredocs.org/clojure.core/flatten) |
| 225 | +;; the buttons presses and then calculating the |
| 226 | +;; [`frequencies`](https://clojuredocs.org/clojure.core/frequencies).\ |
| 227 | +;; The second result is just the `count` of buttons pressed. |
244 | 228 | ;; |
245 | | -;; For example, for the first line in our example: |
| 229 | +;; Here's an example where we press three buttons: |
| 230 | + |
| 231 | +(let [pressed-buttons [[0] [0 1] [0 1 2]]] |
| 232 | + (press-results pressed-buttons)) |
| 233 | + |
| 234 | +;; We get the vector with two results we wanted. The first element is a hashmap |
| 235 | +;; telling us how much the joltage has been change at each index: |
| 236 | +;; index 0 by 3, index 1 by 2, index 2 by 1. |
| 237 | + |
| 238 | + |
| 239 | + |
| 240 | +;; Different button combinations can produce different joltages, |
| 241 | +;; but the same light parity: |
| 242 | + |
| 243 | +(defn light-parity [joltages] |
| 244 | + (->> joltages |
| 245 | + (keep (fn [[k v]] (when (odd? v) k))) |
| 246 | + set)) |
| 247 | + |
| 248 | +(= (light-parity {0 3 , 1 2 , 2 1}) |
| 249 | + (light-parity {0 11 , 1 0 , 2 33})) |
| 250 | + |
246 | 251 |
|
247 | | -(let [[goal buttons _] (first example-data)] |
248 | | - (press-buttons' buttons goal)) |
249 | 252 |
|
| 253 | +;; We will [`group-by`](https://clojuredocs.org/clojure.core/group-by) the |
| 254 | +;; results by `light-parity`. |
250 | 255 |
|
251 | | -;; So we have a way to solve a single recursion step, and now we need |
252 | | -;; to find a way to go to a new state for the next recursion step. |
| 256 | +(defn all-states [buttons] |
| 257 | + (->> buttons |
| 258 | + all-subsets |
| 259 | + (map press-results) |
| 260 | + (group-by (comp light-parity first)) ; [1] |
| 261 | + (#(update-vals % distinct)))) ; [2] |
253 | 262 |
|
254 | | -(defn new-state [joltages presses] |
255 | | - (let [joltages' (reduce #(update %1 %2 dec) ; [1] |
256 | | - joltages |
257 | | - (flatten presses))] ; [2] |
258 | | - (when (not-any? neg? joltages') ; [3] |
259 | | - [(mapv #(quot % 2) joltages') ; [4] |
260 | | - (count presses)]))) |
| 263 | +;; The result of `press-results` is a vector: we need its first element |
| 264 | +;; (joltages) to calculate `light-parity` [1].\ |
| 265 | +;; There will be some duplicated results (we make the same joltage changes |
| 266 | +;; with two different combinations of buttons, with the same number of |
| 267 | +;; button presses), so we will keep only |
| 268 | +;; the [`distinct`](https://clojuredocs.org/clojure.core/distinct) values [2]. |
261 | 269 |
|
262 | | -;; For each result we will calculate the remaining `joltages'` of each light |
263 | | -;; by `dec`reasing the current joltage of that light every time a button |
264 | | -;; has modified it [1]. To convert a nested list of button-presses to |
265 | | -;; a flat list of light-modifications, we use the |
266 | | -;; [`flatten` function](https://clojuredocs.org/clojure.core/flatten) [2]. |
| 270 | + |
| 271 | +(let [[_ buttons _] (first example-data)] |
| 272 | + (all-states buttons)) |
| 273 | + |
| 274 | +;; This result might look like a mess, but it is something we can work with. |
| 275 | +;; And we're close to the finish line. |
| 276 | +;; |
| 277 | +;; So we have a vector of initial joltages that looks like `[3 5 4 7]` |
| 278 | +;; and for some button combination we now have a vector with a |
| 279 | +;; hashmap of changed joltages and the number of button presses, |
| 280 | +;; like `[{1 2, 3 1, 0 2, 2 1} 3]`. |
| 281 | +;; |
| 282 | +;; We'll need to calculate a `new-state` with the remaining joltages: |
| 283 | + |
| 284 | +(defn new-state [joltages [deltas presses]] |
| 285 | + (let [joltages' (reduce-kv (fn [acc idx v] |
| 286 | + (update acc idx - v)) ; [1] |
| 287 | + joltages |
| 288 | + deltas)] |
| 289 | + (when (not-any? neg? joltages') ; [2] |
| 290 | + [(mapv #(quot % 2) joltages') ; [3] |
| 291 | + presses]))) |
| 292 | + |
| 293 | +;; To calculate the remaining `joltages'` of each light at an index `idx`, |
| 294 | +;; we will subtract the amount we've just pressed at the same index [1]. |
267 | 295 | ;; |
268 | 296 | ;; If any of the new light joltages is negative, it means we've reached |
269 | | -;; an illegal state and we'll return `nil` [3]. |
270 | | -;; Otherwise, we need to know the `count` of button presses, and we |
| 297 | +;; an illegal state and we'll return `nil` [2]. |
| 298 | +;; Otherwise, we still need to know the number of button `presses`, and we |
271 | 299 | ;; prepare our next state by dividing the new joltages by two. |
272 | 300 | ;; (Why? It is explained in the linked insight.) |
273 | 301 | ;; |
274 | 302 | ;; Here's an example: |
275 | 303 |
|
276 | | -(let [[_ buttons joltages] (first example-data) |
277 | | - goal (mapv odd? joltages)] |
278 | | - (->> (press-buttons' buttons goal) |
279 | | - (mapv #(new-state joltages %)))) |
| 304 | +(let [joltages [3 5 4 7] |
| 305 | + results [{1 2, 3 1, 0 2, 2 1} 3]] |
| 306 | + (new-state joltages results)) |
280 | 307 |
|
281 | 308 |
|
282 | 309 |
|
283 | | -;; Time to put it all together: |
284 | 310 |
|
285 | | -(def press-buttons-2 |
286 | | - (memoize |
287 | | - (fn [buttons joltages] |
288 | | - (if (every? zero? joltages) 0 ; [1] |
289 | | - (->> joltages |
290 | | - (map odd?) ; [2] |
291 | | - (press-buttons' buttons) ; [3] |
292 | | - (keep #(new-state joltages %)) ; [4] |
293 | | - (map (fn [[joltages' presses]] ; [5] |
294 | | - (+ presses |
295 | | - (* 2 (press-buttons-2 buttons joltages'))))) |
296 | | - (reduce min 100000)))))) ; [6] |
| 311 | +;; How do we get from the initial state to the wanted result? By using |
| 312 | +;; recursion and dividing the problem until we come to the end: |
| 313 | + |
| 314 | +(defn press-buttons-2 [states joltages] |
| 315 | + (if (every? zero? joltages) 0 ; [1] |
| 316 | + (let [parity (light-parity (map-indexed vector joltages))] ; [2] |
| 317 | + (->> (states parity) ; [3] |
| 318 | + (keep #(new-state joltages %)) ; [4] |
| 319 | + (map (fn [[joltages' presses]] ; [5] |
| 320 | + (+ presses |
| 321 | + (* 2 (press-buttons-2 states joltages'))))) |
| 322 | + (reduce min 100000))))) ; [6] |
297 | 323 |
|
298 | 324 | ;; If our recursion reached the state where every remaining joltage is zero, |
299 | 325 | ;; there's no more presses to be made and we return zero [1].\ |
300 | | -;; Otherwise, our current `goal` is defined by parity of the current joltages [2]. |
301 | | -;; We press the buttons to reach that goal [3] and then use the |
| 326 | +;; Otherwise, we calculate the `light-parity` of the current `joltages`. |
| 327 | +;; That function expects a hashmap: a vector with `[index value]` pairs |
| 328 | +;; can disguise as one [2]. |
| 329 | +;; |
| 330 | +;; We are interested only in those `states` with the current `parity` [3]. |
| 331 | +;; For each state we calculate the `new-state` and then use the |
302 | 332 | ;; [`keep` function](https://clojuredocs.org/clojure.core/keep) to |
303 | 333 | ;; return only valid new states [4], as defined above. |
304 | 334 | ;; |
|
311 | 341 | ;; the states explored [6]. |
312 | 342 |
|
313 | 343 |
|
| 344 | + |
| 345 | + |
| 346 | + |
314 | 347 | (defn part-2 [data] |
315 | | - (aoc/sum-pmap (fn [[_ b j]] (press-buttons-2 b j)) data)) |
| 348 | + (aoc/sum-pmap (fn [[_ buttons joltages]] |
| 349 | + (press-buttons-2 (all-states buttons) joltages)) |
| 350 | + data)) |
316 | 351 |
|
317 | 352 | ;; All that is left to do is to call the above function for each row of |
318 | 353 | ;; our input and sum the results. To do this in a bit less time, we will |
|
328 | 363 |
|
329 | 364 |
|
330 | 365 |
|
| 366 | + |
| 367 | + |
| 368 | + |
| 369 | + |
| 370 | + |
| 371 | + |
331 | 372 | ;; ## Conclusion |
332 | 373 | ;; |
333 | 374 | ;; Part 1 was a nice and relatively easy task. |
334 | 375 | ;; |
335 | 376 | ;; Part 2 uses the [brilliant idea](https://old.reddit.com/r/adventofcode/comments/1pk87hl/2025_day_10_part_2_bifurcate_your_way_to_victory/) |
336 | 377 | ;; on how to approach this task and recursively split it into smaller tasks |
337 | | -;; we know how to solve (from Part 1). |
| 378 | +;; we know how to solve. |
338 | 379 | ;; |
339 | 380 | ;; Today's highlights: |
340 | | -;; - `constantly`: create a function that always returns the same value |
341 | | -;; - `cond->`: conditionally update an expression |
342 | 381 | ;; - `keep`: return non-nil results of applying a function to the elements of |
343 | 382 | ;; a collection |
| 383 | +;; - `distinct`: remove duplicates in a collection |
| 384 | +;; - `juxt`: create a vector of applying different functions to an argument |
| 385 | +;; - `frequencies`: count the appearances of elements in a collection |
| 386 | +;; - `group-by`: group elements of a collection by the result of a funciton |
344 | 387 |
|
345 | 388 |
|
346 | 389 | ;; ---- |
|
0 commit comments