🤖 - 2024 DAY 24 SOLUTIONS - 🤖
Day 24: Crossed Wires
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Haskell bits and pieces
The nice thing about Haskell's laziness (assuming you use Data.Map rather than Data.Map.Strict) is that the laziness can do a ton of the work for you - you might've spotted a few Haskell solutions in earlier days' threads that use this kind of trick (eg for tabling/memoisation). Here's my evaluation function:
For part 2, we know what the graph should look like (it's just a binary adder); I think this is a maximal common subgraph problem, but I'm still reading around that at the mo. I'd love to know if there's a trick to this.
Thank you for showing this trick, I knew Haskell was lazy but this one blew my mind again.
Yeah, I remember when I saw this for the first time. It's astonishing how powerful lazy evaluation can be at times.
Dart
Not very happy with this, as for part 2 the code just told me which four pairs of bits of the output needed investigation and I then tediously worked through how they differed from the correct adder implementation in the debugger.
Spoilered as it is overly long and not very interesting.
::: spoiler spoiler
:::
Haskell
Part 1 was trivial, just apply the operations and delay certain ones until you have all the inputs you need.
::: spoiler Code
:::
For part 2 I tried symbolic solving to detect discrepancies but I wouldn't achieve anything with it.
::: spoiler SymbolicEquation
:::
My solution was to use the
dotEngine-function to translate the operations into a digraph in graphviz-style which I simply plotted and searched through using a python script.::: spoiler dotEngine
:::
I took a loook at the initial graph which was a vertical line with a few exception which I figured would be the misordered wires. I did try some hardware-simulations in the far past to build bit-adders which helped me recognize patterns like carry calculation. First I replaced all occurences of
x__ XOR y__ -> wwithx__ XOR y__ -> xor__to recognize them more easily. The same withANDof xs and ys. Using the following script I would then use some Regex to search for the rules that corresponded to carry calculations or structures I knew. The script would break exactly four times and I would then figure out what to switch by hand through looking at the updated graphViz.Please excuse the bad coding style in the script, I had written it on the ipython-REPL.
::: spoiler python script
:::
When solving such a swapped wire problem I would then use my haskell function to plot it out again and stare at it for a few minutes until I understood wich parts belonged where.
The last one looked like this
In this one I needed to switch
jdrandcarry31to make it work.Haskell
For part2 I compared the bits in the solution of part1 with the sum of x and y. With that, I could check the bits that did not match in a graphviz diagram and work from there.
::: spoiler code
:::
Haskell part 2, much better solution
Okay, here's the outline again - this one ran instantly.
Rather than probing with example values, I took a different approach, debugging the structure. I only really care about inputs and outputs, so I wrote something that turns the "wiring diagram" into a map of label -> Expr, where
(the
Eqinstance is stable in symmatric expressions, eg(==) (EAnd a b) (Eand c d) = a == c && b == d || a == d && b == c)The expressions are grounded in "inputs" ("x00".."x44", "y00".."y44") - that is, they just expand out all of the intermediary labelled things.
Then I constructed a circuit that I was after by building a non-swapped 44/45-bit full adder, and produced the same set of expressions for those.
Then: for each output, z00..z45, check the "spec" expression against the actual one. If they're identical, move on.
Otherwise, find some candidate pairs to swap. For these, I considered all possible labelled outputs except "stable" ones - that is, those that were input depdendencies of z_(i-1) - ie, don't swap any outputs involved in the computation that's validated thus far.
Taking the new layout with swapped outputs and its corresponding set of expressions, carry on searching as before.
A linear scan over the output bits was all that was required - a unique answer poped out without any backtracking.
Anyway, happy Christmas all.
PS. My other version worked (eventually) - it was following this approach that led me to realise that my "spec" full adder was broken too :-D Never skip the unit tests.
(@[email protected] you were asking about alternatives to graphviz-style approaches I recall)
Yes, I was, and this is very impressive. This should be a generic solution right? I'll have to work out how to run it and test on my input.
Generic-ish. It'll fit any of the input problems I think. You could fool it by using a non-canonical circuit, because it knows nothing about the equivalence of boolean expressions; and it also relies on one swap sufficing to fix an output, so I didn't go particularly far into turning it into a generic search. Either of those problem extensions would take much more effort from a solver, so my expectation is that they were deliberately avoided.
Haskell, programmatic solution
I spent an entire day on this because I didn't write a unit test to check my "swap outputs" function, which effectively did nothing.
In any case: the approach (which may be more interesting than the code, I know people were interested) involved probing the addition circuit with some example additions - that is, I wrote something that'd let me give alternative inputs from x & y and compute the result using the circuit. I then gave it some simple pairs of values that'd exercise the add and carry bits (ie, pairs chosen from
{i << n, n <- {1..43}, i <- {1, 3}}). That gave me some breaking trials.Because the errors were relatively sparse, I then scanned over pairs of outputs, swapping those that didn't introduce a data dependency and checking (a) that no new errors were introduced over the trial sets, (b) for any reduction in the number of errors found. I got a bunch fo outputs like this:
which found the pairs for me. The search could be improved by more carefully tying the probe inputs to the outputs' dependencies (ie, if the first error comes from the (xi, yi) input bits, then look for swaps of the dependencies introduced by zi) - but in any case, it finds the answer. Phew.
Rust + Pen and Paper
Yikers. Part 2 took a while, staring at this diagram for hours. Eventually I noticed that each of these blocks has two pairs of (XOR, AND) gates sharing the same inputs (and inputs aren't changed). So I matched up these pairs based on a distance metric of how much needs to be swapped to fit together. This helped me identify 4 blocks with errors, the rest was solved using pen and paper (one block is missing as it became apparent at that point):
There is also some code, but do yourself and me a favor and don't look at it. While it does turn up the correct solution, it probably won't with any other input, especially not the examples.
Javascript
Part one was easy, though despite starting at midnight I only placed 1786 for part one. I think my tendency to want to do OOP makes it take longer...
Part two.. Well, I figured it was some sort of binary circuit for trying to add binary numbers. So I hoped that the sum of the x registers and the y registers was the expected result of simulating the circuit like in part one. I later verified that it is the expected result.
I didn't want to try and manually figure out the bad outputs, coffee wasn't helping, I wanted sleep. So I uh.. I wrote logic to randomly create swaps. And then just hoped RNG got me covered. To help my chances, I ran it on 8 different processes.
When I woke up in the morning I discovered 8 stopped processes, each with "a solution" that was different. Turns out, if you just randomly swap wires at some point you get a system that outputs the desired result - but only because you sufficiently screwed it up more to produce the expected result, even if the system itself would not work for other input.
I could probably change the registers to another value, run it, and see if they match, thus ruling out an incorrect set of swaps causing a correct result with the original binary inputs. But at this point I just decided to do it the manual way following advice on here. My brain is fried, I'm stepping away to take a shower and get ready to visit family.
I had really hoped the bruteforce would work, I modified the bruteforce to run even after it finds a match and I'll let it run while I'm gone today and see if RNG produces any correct result at some point - I just fear the exponential answer timeout will prevent me from submitting these correctly incorrect combinations lol. I might modify it later with my theory above and just run it on a server indefinitely and see if it produces the correct result eventually.
https://blocks.programming.dev/Zikeji/9e4d6e81595d4845b88cf98eb91852d8
Edit:
Created a raw multithreaded bruteforce variant: topaz
Raku
I resisted the urge to solve part 2 manually and I was eventually able to get a working solution. Well it's an approximate solution, taking advantage of the AoC input being not-so-mungled. I ended up using a couple of validity checks for part2. Specifically:
This also takes advantage of the fact that all the inputs are correct, it is only gate outputs that is messed up. Using this fact you can distinguish between XOR gates that are calculating the partial addition of xXX + yXX and the XOR gates used to get the final zXX bit. Same story for the AND gates.
And I guess this also takes advantage of the fact that the input circuit is a standard adder without any extra gates.
::: spoiler Code
:::
Haskell
For completeness' sake. I actually solved part 2 by looking at the structure with Graphviz and checking the input manually for errors. So the code here merely replicates the checks I was doing by hand.
::: spoiler solution
:::
Oh my Kernigan, that was stressful. Really worried about not finishing there.
Considered several approaches, the coolest of which would have been to test individual bits, propagate 'suspicion', etc, but it seemed too tricky.
Eventually I needed to go do something other than worry about not finishing so I started writing a validator for the adder structure. Just a couple of rules later I had found 4 faults already and managed to write automated fixups for them!
This means my solver is quite specific to my input but it can potentially be made more complete and I didn't 'cheat' by hardcoding manual graph analysis.
::: spoiler Code
:::
https://codeberg.org/sjmulder/aoc/src/branch/master/2024/c/day24.c
Btw, spending some time on getting Graphviz output right did make studying the structure much easier!
I fiddled for ages to get nice graphviz output, that looks a lot nicer