- 10 Dec, 2025 *
Party people! We’re back for day 10 of Advent of Code. And I’ve got to say: thank God for Python libraries. Again. Why, you say? Well, let’s take a look.
Check out my full solution for day 10 at GitHub.
We’ve entered the Elves’ factory and like always, things are off. This time, all the factory machines are broken and we need to help the Elves to get them back online. Fortunately, they have a manual for each machine, with instructions on how to get them back online. For each machine, we get a line of input in the following format:
[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
The first part between brackets show what the status of the indicator…
- 10 Dec, 2025 *
Party people! We’re back for day 10 of Advent of Code. And I’ve got to say: thank God for Python libraries. Again. Why, you say? Well, let’s take a look.
Check out my full solution for day 10 at GitHub.
We’ve entered the Elves’ factory and like always, things are off. This time, all the factory machines are broken and we need to help the Elves to get them back online. Fortunately, they have a manual for each machine, with instructions on how to get them back online. For each machine, we get a line of input in the following format:
[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
The first part between brackets show what the status of the indicator lights should be, with a period (.) meaning a light should be off and a hashtag (#) meaning a light should be on. The middle part indicate buttons that trigger the corresponding indicator light on or off (based on its current status) and the last part between curly brackets are joltage requirements (which come into play in part two).
Part one
Our job for the first part is to push the buttons in a sequence that results in the indicator lights matching the status from the manual. For example, if we press the button (0,2) followed by the button (0,1), we first turn on the lights at index positions 0 and 2 (resulting in state [#.#.]) and then toggle the lights at index positions 0 and 1 (resulting in state [.##.]).
In other words, whenever we press a button that toggles an index position, we "flip" the light at that position. To me this sounded a lot like binary digits (or bits) that toggle between 0 and 1 each time. So if we represent each button with binary digits and try to combine them with some combinations of buttons, we might eventually end up with the correct status for the indicator lights.
In the example from before, where we pressed buttons (0,2) and (0,1), we can look at these buttons as binary digits, its length being the same as the number of indicator lights (in this case, there are four lights). Button (0,2) means that positions 0 and 2 in the binary digit are set to 1 and the other positions are set to 0:
- Button
(0,2)becomes1010. - Button
(0,1)becomes1100.
Combining these binary digits using an XOR operator (where the resulting bit on an index position is equal to 1 if and only if there is a single 1 at that position) we get:
1010 ^
1100
______
0110
Which is the status for the indicator lights we’re looking for. So for this machine to turn on, we need to press two buttons. This principle can be used for all input lines, using code like this:
def start_machine(button_wirings_bitmask: List[str], light_diagram: int) -> List[str]:
button_wirings = [bitmask_to_int(b) for b in button_wirings_bitmask]
buttons_length = len(button_wirings)
for r in range(1, buttons_length + 1):
for combo in combinations(range(buttons_length), r):
xor_value = 0
for i in combo:
xor_value ^= button_wirings[i]
if xor_value == light_diagram:
return [button_wirings[i] for i in combo]
Note that the button representations have already been transformed into binary digits in a helper-function that I used while reading the input, which is why they need to be cast back to integers (decimal numbers). The general idea of this function is to generate multiple combinations of buttons (using combinations() from itertools) and XOR them together with the ^= assignment operator. When we find a state for the indicator lights that is equal to the expected state, we know we’re done and return the combination of buttons that have been pressed to get to this state.
In the main function for part one we count the number of buttons that start_machine() returns and sum the result for each line to get the answer for part one:
def part_one(data: List[str]) -> int:
button_presses = 0
for (light_diagram, button_wirings, _) in data:
...
result = start_machine(button_wirings_bitmask, bitmask_to_int(light_diagram))
button_presses += len(result)
return button_presses
Part two
The second part of recent days - and today is no exception - fall in the category "thank God there’s a Python library for that". Because without one, I wouldn’t have been able to solve today’s part two.
The joltage requirements that have been ignored in part one are becoming relevant in part two. Let’s consider the first input line again:
[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
The last part of this line, {3,5,4,7}, shows us how many times we need to press a button at an index position. So, the first 3 in the joltage requirements means that we should press the button on index position 0 three times, press the button at index position 1 five times, at index position 2 four times and at index position 3 seven times. We can again use the buttons on the machine to reach this state (where the first button (3) means that index position 3 in increased by one, button (1,3) means that the index positions 1 and 3 are increased by one, etcetera).
I’ve thought long and hard about this, thinking that I should be able to solve this with something binary-related. Maybe with some other bitwise operator. But I couldn’t figure it out. Until my search online stumbled upon something called Integer Linear Programming (ILP). In short, this is a mathematical approach to solving problems, where we choose integer values for some variables, so that certain linear equations are satisfied while minimizing or maximizing something.
And honestly, that sounds like something we could use. We have integer values (binary digits in this case) that represent buttons, we can think of a linear equation that needs to be satisfied, and we need to minimize something in the form of the least number of button pressed per machine.
In the same example as before, we have the following buttons:
(3) (1,3) (2) (2,3) (0,2) (0,1)
which can be represented as vectors (with the button index positions as binary digits with the same length as the number of indicator lights):
v0 = (0,0,0,1) for (3)
v1 = (0,1,0,1) for (1,3)
v2 = (0,0,1,0) for (2)
v3 = (0,0,1,1) for (2,3)
v4 = (1,0,1,0) for (0,2)
v5 = (1,1,0,0) for (0,1)
For each vector, we know that we need to press it zero or more times (with a variable: x times). A combination of pressed should lead to the final state for the indicator lights, with the joltage requirement {3,5,4,7} showing us that index position 0 needs to be pressed three times, and the next index positions need to be pressed five, four and seven times, respectively.
Mathematically, that can be written as:
(x0 * v0) + (x1 * v1) + (x2 * v2) + (x3 * v3) + (x4 * v4) + (x5 * v5)
which is the linear equation ILP needs from us.
In Python, the library pulp can help in setting up an ILP problem that needs to be solved. It does this in a few steps:
First, we initialize the optimization problem:
problem = LpProblem("configure_machine", LpMinimize)
Next, we initialize the variables x, one for each button vector:
variables = [
LpVariable(f"x_{i}", lowBound=0, cat="Integer")
for i in range(len(vectors))
]
Then we add each variable to the problem statement:
problem += lpSum(variables)
And then the important part: we add the constraint we have, which is that the final result of the linear equation should adhere to the joltage requirement:
for i in range(len(target)):
problem += lpSum(vectors[j][i] * variables[j]
for j in range(len(vectors))) == target[i]
This piece of code might be the hardest to understand. It certainly was for me. The key here is that we need to know what values (from v0 up to v5) contribute to each coordinate, e.g. what buttons trigger what index positions?
If we visualize this in a table with the coordinates on the columns (index positions of the joltage requirement) and the buttons (vectors) on the rows, we get:
coord0 coord1 coord2 coord3
v0 = 0 0 0 1
v1 = 0 1 0 1
v2 = 0 0 1 0
v3 = 0 0 1 1
v4 = 1 0 1 0
v5 = 1 1 0 0
If we look in the first column (coord0) as an example, we see that vectors v4 and v5 contribute to this index position. As this index position needs to have 3 as the final result (as per the joltage requirement), we know that only vectors v4 and v5 can help us get to this result:
0·x0 + 0·x1 + 0·x2 + 0·x3 + 1·x4 + 1·x5 = 3
Each equation forces one coordinate of the total sum to match the target coordinate.
Finally, we ask ILP to solve the problem for us:
problem.solve(PULP_CBC_CMD(msg=False))
and finally, we return the result of this equation as the result for this line (and this machine). The final result for part two is the sum of all results per line (and machine).
How the ILP does things internally? Your guess is as good as mine, and honestly, that’s math that goes beyond my current knowledge. It performs some magic to shrink the number of possible solutions in a couple of steps, so the resulting set of possible solutions becomes small enough for this problem to be solvable in a respectable amount of time (~2 seconds for me). But the exact details are up for grabs, and I invite you to research those on your own.
Again, this is why I love Python libraries. On to day 11!