Skip to content

Commit b20230a

Browse files
committed
day 10, part 2: 5x faster
1 parent 072edc5 commit b20230a

2 files changed

Lines changed: 163 additions & 120 deletions

File tree

clojure/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ Day 6: [Trash Compactor](https://adventofcode.com/2025/day/6) | [d
2828
Day 7: [Laboratories](https://adventofcode.com/2025/day/7) | [day07.clj](src/day07) | fnil, memoize | | I smell something Lanternfishy.
2929
Day 8: [Playground](https://adventofcode.com/2025/day/8) | [day08.clj](src/day08) | hash-set, disj | | No Manhattan distance? Wow!
3030
Day 9: [Movie Theater](https://adventofcode.com/2025/day/9) | [day09.clj](src/day09) | every?, pmap, ffirst | | The hardest one so far.
31-
Day 10: [Factory](https://adventofcode.com/2025/day/10) | [day10.clj](src/day10) | constantly, cond->, keep | | Divide and conquer.
31+
Day 10: [Factory](https://adventofcode.com/2025/day/10) | [day10.clj](src/day10) | keep, distinct, juxt, frequencies, group-by | | Divide and conquer.
3232
Day 11: [Reactor](https://adventofcode.com/2025/day/11) | [day11.clj](src/day11) | memoize, zero? | | Surprisingly easy.
3333
Day 12: [Christmas Tree Farm](https://adventofcode.com/2025/day/12) | [day12.clj](src/day12) | | | The most disappointing AoC task ever?

clojure/src/day10.clj

Lines changed: 162 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{:title "Factory"
44
:url "https://adventofcode.com/2025/day/10"
55
:extras ""
6-
:highlights "constantly, cond->, keep"
6+
:highlights "keep, distinct, juxt, frequencies, group-by"
77
:remark "Divide and conquer."
88
:nextjournal.clerk/auto-expand-results? true
99
:nextjournal.clerk/toc true}
@@ -52,7 +52,7 @@
5252
;; To convert the `lights` into something we can use later, we remove the first and
5353
;; the last character (`[` and `]`, respectively), and the rest we convert to
5454
;; 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.)
5656
;;
5757
;; For `joltages`, we just need to extract all integers from the last element [3].
5858
;; We do the same thing for _each_ `button` [4].
@@ -123,52 +123,47 @@
123123

124124

125125
(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.
152147
;;
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].
154149
;;
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
156151
;; found a new best result (we will see in a minute why this is true), and
157152
;; we continue exploring the rest of the states in attempt to find an even
158-
;; better result [4].
153+
;; better result [2].
159154
;;
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
163158
;; (`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
165160
;; were. (Notice the lack of `'`.)
166161
;;
167162
;; We add both those scenarios to the `states'` we wish to explore next with
168163
;; the remaining `buttons'`.
169164
;;
170165
;; Otherwise, we explore the remaining `states'` to see if we can improve
171-
;; the best result [9].
166+
;; the best result [7].
172167

173168
(press-buttons (first example-data))
174169

@@ -197,108 +192,143 @@
197192
;;
198193
;; Thanks to [this brilliant insight](https://old.reddit.com/r/adventofcode/comments/1pk87hl/2025_day_10_part_2_bifurcate_your_way_to_victory/),
199194
;; 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
201196
;; definitely read it.
202197
;;
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
238217
;;
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.
244228
;;
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+
246251

247-
(let [[goal buttons _] (first example-data)]
248-
(press-buttons' buttons goal))
249252

253+
;; We will [`group-by`](https://clojuredocs.org/clojure.core/group-by) the
254+
;; results by `light-parity`.
250255

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]
253262

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].
261269

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].
267295
;;
268296
;; 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
271299
;; prepare our next state by dividing the new joltages by two.
272300
;; (Why? It is explained in the linked insight.)
273301
;;
274302
;; Here's an example:
275303

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))
280307

281308

282309

283-
;; Time to put it all together:
284310

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]
297323

298324
;; If our recursion reached the state where every remaining joltage is zero,
299325
;; 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
302332
;; [`keep` function](https://clojuredocs.org/clojure.core/keep) to
303333
;; return only valid new states [4], as defined above.
304334
;;
@@ -311,8 +341,13 @@
311341
;; the states explored [6].
312342

313343

344+
345+
346+
314347
(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))
316351

317352
;; All that is left to do is to call the above function for each row of
318353
;; our input and sum the results. To do this in a bit less time, we will
@@ -328,19 +363,27 @@
328363

329364

330365

366+
367+
368+
369+
370+
371+
331372
;; ## Conclusion
332373
;;
333374
;; Part 1 was a nice and relatively easy task.
334375
;;
335376
;; Part 2 uses the [brilliant idea](https://old.reddit.com/r/adventofcode/comments/1pk87hl/2025_day_10_part_2_bifurcate_your_way_to_victory/)
336377
;; 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.
338379
;;
339380
;; Today's highlights:
340-
;; - `constantly`: create a function that always returns the same value
341-
;; - `cond->`: conditionally update an expression
342381
;; - `keep`: return non-nil results of applying a function to the elements of
343382
;; 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
344387

345388

346389
;; ----

0 commit comments

Comments
 (0)