5
votes
Day 10: Factory
Today's problem description: https://adventofcode.com/2025/day/10
Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):
<details>
<summary>Part 1</summary>
```python
Your code here.
```
</details>
Wow, what an escalation in difficulty!
For part 1, it's clear that the order in which buttons are pressed doesn't matter and that it's useless to press a button more than once since pressing twice is equivalent to not pressing and we want to minimize the number of presses. This problem can be represented very efficiently as a bitset and xor operations, where each bit corresponds to a light.
My solution was to iterate over all the combinations of pressed and not pressed for each button and see if the xor of all the pressed buttons matches the required light pattern. I think it would be more efficient to iterate over all the permutation of
nbutton pressed, increasingnuntil we find a match. But with at most 13 buttons in my input, in the worst case I'll test 8192 patterns, which is pretty fast since I'm only doing some basic bit operations for each iteration. Runtime for this was ~500μs.For Part 2, I'm converting the problem into a system of equations, with one equation for each joltage. For the
nth joltage, I have an equation resemblingsum b_k = joltage[n], whereb_krepresents the number of times thekth button is pressed. The sum is only on buttons that turn on thenth light. For example:Obviously, this example doesn't have a solution, but hopefully the idea is clear. Importantly, all the
b_iare known to be integers and positive or 0.I have two ways to know for sure that a variable is equal to a given value:
something = 0, then all the corresponding variables are 0.b_i = x, then I know the value of this variable.For all the known values, I can substitute them in the existing equations.
Additionally, I also have some logic to create more equations that use the same form (i.e. a sum of variables without any multiplication) by identifying cases where one equation contains all the variables of another equation. For example:
This implies that
b_1 = 5, which feeds into the logic where I know the value of a variable and can further simplify the system.This is actually enough to solve a good 40% of the machines in my input. However, that's not enough for all of them. So once I have simplified a system as much as possible, I identify the equation with the smallest constant, pick a variable in that equation and try solving the system assuming the picked variable is equal to any value from 0 to the constant. If I get no solution, the system cannot be solved and if I get multiple solutions, I keep the one leading to the smallest number of button presses.
Importantly, when doing all the operations on the system, I make sure that I don't have any issues, like an equation with a negative constant, or multiple equations with the same variables but different constants. If this happens, I know the system has no solution.
In terms of implementation, the left side of all those equations can be efficiently represented as bitsets. For example
b_1 + b_3 + b_4can be represented as0b11010. Finding equations with a single variables corresponds to finding numbers that are powers of 2 (but I just use the count_ones() method). Finding equations containing all the variables of another can also be done with a few bit operations:a & b == ameansbcontains all the bits thatacontains.One optimization I didn't do is to use a bound on the number of button presses when solving the systems. Once I have a solution, if any partial solve would lead to more button presses than the known solution, there is no need to continue solving the system.
Anyway, with all of this, part 2 runs in ~240ms.
Solution (Rust)
I'd be curious if anyone finds a nicer solution for part 2, outside of just calling any off-the-shelf linear integer programming solver.
Thanks for pointing this out! I fixed the message to avoid confusing other people. Hopefully, I did a good enough job to also avoid having people later confused by your message :)
I continue to be in awe of your contributions.
Thanks for taking the time to do these little writeups!
As I'm typing this, my code is still running. Probably not a great sign, but if it works it works! (It's yet to be seen if it works).
Similar massive escalation in difficulty between Part 1 and Part 2 today as to yesterday. Unlike yesterday, I did know how to solve Part 2, I just didn't know how to do it efficiently (and as we pass the 5 minute mark, it's clear that I never figured that out). I started with a basic recursive depth-first search, because I like to be extra inefficient and couldn't remember how to do a BFS off the top of my head. It "worked" for the sample data, but it was clear that it would probably take several days of processing to get the answer with the real data. Then I remembered that caching/memoization was a thing. That sped it up considerably, but also got the wrong answer sometimes, because DFS was the wrong tool for this.
So then I looked up how to implement a BFS, I cached results again, and now at the 10 minute mark, my code is still executing. At least the sample data processed quickly. With 177 independent inputs, I guess this is what parallelization is for.
I'm posting this at the 30 minute mark in case my computer crashes. I wish I'd added some logging statements so that I knew it was progressing. I'll update with the code if it turns out it actually worked.
EDIT: Killed the process after ~2.5 hours. Peeked at some AoC threads, and saw that this is something called Integer Linear Programming. I don't think I covered that in university, but if I did, I've forgotten all about it. But luckily, I can use google and found a tool called PuLP that I managed to figure out well enough to use. Not sure this really counts as "my" solution at this point, but it's at least a solution, and it runs in a couple seconds. Might actually be in milliseconds, but the console output is pretty slow.
Part 1 & 2 [Python]
For your BFS, do you mean that each node in your graph represents a list of values corresponding to the number of times each button is pressed?
If that's the case, won't this mean you're going to explore at least all the nodes [x, y, z, ...] with each variable in the range
[0, number of times it needs to be pressed in the optimal solution]? The number of such nodes is going to be the product of the values x, y, z, ... for the optimal solution. Unless I have made a mistake somewhere, the worst machine in my input has a product of number of presses in the optimal solution of about 10 billions. And that's not even including all the nodes that don't lead to an optimal solution. It's not obviously yet in the "longer than the age of the universe" range of runtime, but I wouldn't bet that it's going to finish in a reasonable amount of time either, unless visiting each node is really quick. Also, if the cache doesn't flush all the nodes that are not going to be visited again (sum of [x, y, z, ...] less than what you're currently visiting), you're also likely going to end up with memory problems, since storing those 10 billion nodes times something like 8 buttons means storing at least 80GB of data.Yes, that seems likely. A few days ago I crashed my computer by using too much memory in one of these puzzles, and this time it's been running for 2+ hours and my computer is still working, so I think I must have not done everything incorrectly, but this definitely feels more like "age of the universe" runtime and less "I can just let it run overnight".
It has been a very long time since I've worked with problems like this (does it show?). Seems like I need to study math and try again after work.
If you're checking every possible combination of button presses, yeah you are probably going to be waiting for eons. I don't think this is the kind of puzzle that can be solved this way; the numbers are just too big.
I'm in the same boat.
My code for part 2 also probably (tm) technically works, but I'm not gonna run it.
It takes about 2.5 minutes for the demo input, which spells disaster for my actual input, which not only has ~60 times the machines, but those machines are also of a higher complexity, so the actual processing time would probably be much more than 60 times of the demo data, since the bulk of the processing is in trying all combinations of buttons of size n, especially once n crosses 10.
Part 1 also took quite a while and actually filled my RAM before I switched from lists to lazy streams.
I managed to get the first part to ~15 seconds, which is utterly glacial in comparison to what Berdes is putting out, but was a big progress.
There's still most optimization potential on the table. After being a bit sour on part 2 yesterday, I took it lightly and just tried to do it in any way first. But on the upshot, I got some practice with lazy streams and also with Elixir's very simple concurrent computation, which only took a few changed lines.
It's fun to see the PC actually quickly use 100% of all cores here, but it's clearly not the way to solve this.
I think I'll just aim to do part 1 from here on out and try to return with a little time during the holidays to finish the last day 2s without destroying my sleep schedule during the work week.
Well I made it to Day 10 Part 02. I believe this is where I admit defeat.
Part 01 was straightforward enough. I parsed the lights as a bitmap and the buttons as bitmasks used to toggle the lights by XORing them together. A breadth-first search of the permutations finds a solution in 160ms without any optimization.
Part 02 not so much. I recently read a blog post--SIMD in Pure Python--about using the SIMD built into Python's
inthandling. Effectively packing all of the joltage counters into a single integer to do all of the additions simultaneously. It's a neat idea and works well for scaling horizontally across the number of joltages. But does nothing for the size, which appears to go up to 255 in my input. There's not enough room in the universe, let alone my aged ThinkPad, to handle all of the permutations of that many button presses.I've done some optimization--like pruning branches that are overjolted using a quick bitmask. But I think this is a dead end, and I'm out of ideas. Short of some very lucky stochastic methods, this one shall remain unsolved. (Without cheating anyway.)
Part 1
Part 2 (Given a Sufficiently-large Computer/Universe)
Whew, this was difficult... I came very close to throwing in the towel. I'm not the strongest with linear algebra, so part 2 took me a lot of Googling and looking at examples of Gaussian elimination. I eventually arrived at a solution that runs in just over a second on my machine, which I'm happy enough with. I ended up with a lot more code than I'd prefer for an Advent of Code problem, but maybe that's just the way it has to be with today's puzzle.
Explanation
For part 1, I just iterated through all the combinations until I found one that worked. I wanted to iterate through them in ascending order of number of presses, so that I didn't have to go through every combination - after some searching I discovered Gosper's hack, which let me do it with bitsets (which is maybe more efficient than the alternative?).
Part 2 was rough. I realized I needed to solve linear systems almost immediately, but actually doing so was another story. I've never really done linear algebra before, so it took me a while (and a couple hints taken from other people's descriptions of their solutions) to figure out how to even start. What I ended up doing is:
Solution (Lily)
Elixir
I'll need to revisit part 2 when I have a good chunk of time to refresh my linear algebra knowledge. I haven't really touched that stuff in 10+ years, and never in a programming context.
I might dip my toes into Elixir's scientific computing ecosystem for this. It includes Elixir-based versions of many counterparts from Python-land, like
numpy,polars/pandas, Jupyter notebooks, andpytorch.Anyway,
Part 1
Like some others, I noticed that the buttons could be represented as bitmasks that are XOR'ed with 0 to try and produce a target integer: the light diagram, also represented as a binary number.
I use an additional "meta-bitmask" for selecting which of the button bitmasks to apply, since we need to check the entire power set of the buttons to find the minimal number of presses. The best way I could think of to generate a power set of S is to iterate through [0, 2(cardinality of S) - 1], using the iterated value as the meta-bitmask.
Some of my code (particularly the comprehensions) looks a bit cursed.
Elixir is a very... punctuation-rich language.
Benchmarks