You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/2025/puzzles/day10.md
+390Lines changed: 390 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,6 +6,396 @@ import Solver from "../../../../../website/src/components/Solver.js"
6
6
7
7
https://adventofcode.com/2025/day/10
8
8
9
+
## Solution Summary
10
+
11
+
The input includes multiple lines, each of them can be handled independently, the result is the sum from the output of each scenario.
12
+
13
+
For part 1, I applied the [Breadth-first Search](https://en.wikipedia.org/wiki/Breadth-first_search) algorithm (BFS), BFS allows finding the shorest-path from a state to another when every transition has the same cost, this was quicky to implement and produced the correct output.
14
+
15
+
For part 2, BFS wasn't adequate due to the number of potential states, there is an alternative [Divide-and-conquer](https://en.wikipedia.org/wiki/Divide-and-conquer_algorithm) approach which is fast enough.
16
+
17
+
18
+
## Parsing the input
19
+
20
+
Let's represent the input scenario as a case class:
The BFS algorithm is adequate because in every step we can take a button to produce a new state, we have to avoid visiting the same state more than once.
43
+
44
+
For example, given this input `[####] (0) (3) (0,1) (1,2)`:
45
+
46
+
- The target state is `####` (all lights turned on).
47
+
- The initial state is `....` (all lights off).
48
+
- The buttons allow toggling light `0` or `3`, or, lights `0,1` or lights `1,2`.
49
+
50
+
I have created a few helper functions:
51
+
52
+
-`flip` receives the state from a light and toggles its value, `.` to `#`, and, `#` to `.`.
53
+
-`transform` receives the lights state and toggles the lights covered by a button.
source.toList.zipWithIndex.map { case (value, index) =>
59
+
if (button.contains(index)) flip(value)
60
+
else value
61
+
}.mkString("")
62
+
}
63
+
```
64
+
65
+
The way to implement a BFS requires a `Queue`, a `State`, and, a way to track the visited values, for what I used a `Set`.
66
+
67
+
The state for the BFS can de defined with the current light's state and the number of steps required to get there:
68
+
69
+
```scala
70
+
caseclassState(value: String, steps: Int)
71
+
```
72
+
73
+
Naturally, the initial state would have `value` to be only dots (`.`) with `0` steps.
74
+
75
+
Given everything described until now, it is only required to define the BFS process, for this I used a tail-recursive function that does the following:
76
+
77
+
- When the queue is empty, there is no solution.
78
+
- When the current state has reached the goal, return the number of steps.
79
+
- Otherwise, find the new states that can be visited, push them to the queue and repeat.
.getOrElse(throw new RuntimeException("Answer not found"))
189
+
}
190
+
```
191
+
192
+
This resolved the example input but was too slow with the actual test scenarios, I tried a few heuristics to trim unnecessary paths, tried `DFS` with prunning, `A*`, and, I was close to implement a [bidirectional BFS](https://en.wikipedia.org/wiki/Bidirectional_search).
193
+
194
+
Eventually, I got an idea from [reddit](https://old.reddit.com/r/adventofcode/comments/1pk87hl/2025_day_10_part_2_bifurcate_your_way_to_victory/) about using a [Divide-and-conquer](https://en.wikipedia.org/wiki/Divide-and-conquer_algorithm) approach instead which goes like this, if the path from `0` to `T` takes `N` steps, the path from `0` to `2T` takes `2N` steps, with this:
195
+
196
+
- We can try to get convert the `joltage` into even numbers.
197
+
- Resolve `joltage / 2`.
198
+
- The answer for the current step would be `f(joltage / 2)*2 + currentCost`.
199
+
- There are many overlaping sub-problems but we can use a cache to avoid unnecessary recomputation.
200
+
201
+
**DISCLAIMER** I have no proof that this handles every possible scenario but with the test cases I prepared, the BFS result leads to the same from this, and, the result has been accepted by Advent of Code.
202
+
203
+
Let's start by defining how to press the buttons to get `` from the alternatives to resolve part 1, I mentioned that applying a button more than once is not necessary because it invalidates the previous action, in this case with integer values, we can claim the same but it is not about the value itself but the parity.
204
+
205
+
For example, applying a buton `0, 3` to joltages `3, 4, 5, 6` leads to joltates `2, 4, 4, 6` (all even) but applying the same button again will revert the parity back.
206
+
207
+
When all joltages are even (like `2, 4, 4, 6`), we can divide them by 2, leaving us with joltages `1, 2, 2, 3`, then, we apply the same process recursively.
208
+
209
+
Having said this, let's generate all possible transitions given the available buttons, this is, all subsets from the given buttons, leveraging the powerful Scala stdlib, we can call the `Set#subsets` function:
**NOTE**: The empty subset is important because that allow us to take the existing joltages which could be already divisible by 2.
224
+
225
+
The code to generate the moves generates the transformation vector for every button, for example, in the case of a button `0,2`, with `4` joltages, the transformation vector becomes `1,0,1,0`, this is the delta to apply to the joltages with the cost equal to the number of buttons required to get this combination:
226
+
227
+
```scala
228
+
valallMoves= inputCase.buttons.toSet.subsets().toList.map { set =>
229
+
set.foldLeft(IndexedSeq.fill(inputCase.joltage.size)(0) ->0) { case ((acc, cost), button) =>
230
+
valnewVector= acc.zipWithIndex.map { case (x, index) =>
231
+
if (button.contains(index)) x +1
232
+
else x
233
+
}
234
+
newVector -> (cost +1)
235
+
}
236
+
}
237
+
```
238
+
239
+
Essentially, `allMoves` have the transformation options associated with the cost to apply them.
240
+
241
+
We'll require a cache to avoid recomputing the same value twice:
242
+
243
+
```scala
244
+
// min moves to switch the given joltages to 0
245
+
varcache=Map.empty[IndexedSeq[Int], Option[Int]]
246
+
```
247
+
248
+
At last, the actual function to compute the cost required to transform the given joltages into 0s:
249
+
250
+
- When joltages is composed by only 0s, we have the answer (no cost).
251
+
- When the answer for the given joltages is already cached, reuse it.
252
+
- Otherwise, apply any transformations that lead to even-joltages, resolve the smaller problem and compute the answer, out of those options, keep the minimum cost and put it into the cache.
253
+
254
+
```scala
255
+
deff(joltage: IndexedSeq[Int]):Option[Int] = {
256
+
if (joltage.forall(_ ==0)) Option(0)
257
+
elseif (cache.contains(joltage)) cache(joltage)
258
+
else {
259
+
valchoices= allMoves.flatMap { case (delta, cost) =>
0 commit comments