Advent of Code 2024 - Day 24Crossed Wires
| Problem statement | Source code | Tags: PuzzleConstraint satisfactionManual inspection
This problem pays tribute to 2022 day 23.
Part 1
On realizing that the circuit isn't strictly a tree but more like a graph where each node can have multiple inputs and outputs (identical ones but going to different places), I decided to not parse the circuit as a tree, but just as a map instead. I can still do recursive evaluation, and I can also easily memoize the results of each gate, which saves some time reevaluating the same gate for different output bits.
Initially values just contains the input bits; it gets populated as we evaluate the gates, and the same gate is never recomputed.
Part 2
Conceptually, a full adder takes inputs , , and , and produces outputs and , as follows:
A half adder does not have a carry input, which means that is always 0.
Let's print a few outputs:
Take z02 as an example. From the mathematical formula, we know that z02 = x02 ^ y02 ^ C01, where C01 = (x01 & y01) | (C00 & (x01 ^ y01)), where C00 = x00 & y00. This matches the actual printed output, so it looks like z02's structure is as expected.
More generally, it appears that each full adder has the following structure (because of commutativity, the two operands' order may change, but the order of operations is the same):
The first adder, a half adder, has this structure:
The last adder is a full adder, but its carry44 output is labeled z45 instead.
But now we know that some gate labels are swapped, meaning that, while the right-hand side is accurate about the structure of the circuit, the left-hand side may be swapped. For example, the real data may say this:
By swapping foo[n] and bar[n], if we see abc = xn ^ yn, and we just naïvely assume that abc is foo[n], then we would be surprised that we never see carry[n-1] ^ abc anywhere, because actually abc is bar[n]. Now the question is, what do we know? What can we be certain about?
- First, because
ngoes from 0 to 44, we know that there are 44 full adders and 1 half adder, so 44 OR gates, 89 XOR gates, and 89 AND gates. - We also know that the operands of each gate are correct, so for example if we know the correct mapping for
carry[n-1], then the operand it ANDs and XORs with must befoo[n]. - Certain "swaps" are immaterial. For example,
carry[n-1]andfoo[n]are always used together, so swapping their labels do not change the meaning at all. Similarly,bar[n]andbaz[n]are equivalent label-wise.
However, this is about the information that the right-hand side in isolation can tell us: that a label belongs to the carry[n-1]/foo[n] group or the bar[n]/baz[n] group. It does not tell us exactly which one it is. To do so, we must take the potentially-faulty left-hand side into consideration. Fear not, there are only very few faulty lines, and if we can immediately tell which ones are "suspicious", we can safely trust the rest to give us the extra information.
We can start by categorizing each label as "carry or foo" or "bar or baz".
Using these, we can extract the "suspicious" lines. A line is not suspicious if it follows the template given above:
I did not see this coming but the list of unexpected gates turns out to contain exactly 8 lines...? After all, there could both be false negatives because, for example, foo[10] and foo[11] could be swapped, which would not be detected by this logic.
Also! I don't even have to know which pairs are swapped, because the problem asks to sort them all together. So I just literally print these.
Perhaps a bulk of the analysis above isn't necessary for this problem; I would challenge anyone to make a fully generic anomaly detection algorithm that works with the false negatives mentioned above.