Skip to content

Commit 7578361

Browse files
committed
Add solution to 2024-12-24
1 parent 2aec849 commit 7578361

File tree

1 file changed

+89
-0
lines changed

1 file changed

+89
-0
lines changed

2024/day24/solutions.py

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from itertools import combinations
2+
from operator import and_, or_, xor
3+
4+
import networkx as nx
5+
from z3 import BitVec, Extract, Solver, sat, unsat
6+
7+
with open("input") as f:
8+
gs = [g.split("\n") for g in f.read().strip().split("\n\n")]
9+
10+
# Part 1
11+
# Complete hack: Convert the input to Python, then execute it.
12+
for line in gs[0]:
13+
l = line.split(": ")
14+
exec(f"def {l[0]}(): return {int(l[1])}")
15+
16+
str_to_op = {"AND": "and", "XOR": "^", "OR": "or"}
17+
for line in gs[1]:
18+
l = line.split()
19+
exec(f"def {l[4]}(): return {l[0]}() {str_to_op[l[1]]} {l[2]}()")
20+
21+
print(sum(eval(f"z{i:02}()") * 2**i for i in range(46)))
22+
23+
# Part 2
24+
# For later, it will be convenient to track the distance from a broken
25+
# register, so we will set up a graph for that purpose.
26+
G = nx.Graph()
27+
28+
# The plan is to fix the circuit by identifying bits of z that are broken
29+
# one at a time. We will use Z3 to decide whether there exists inputs x, y, z
30+
# such that x + y = z, with gates doing what they are supposed to, but such
31+
# that the j'th bit of z is different from the value in the register z{j}.
32+
# Once such a bit is found, we will try all swaps of the target registers
33+
# until the bit is fixed.
34+
x = BitVec("x", 46)
35+
y = BitVec("y", 46)
36+
z = BitVec("z", 46)
37+
38+
regs = {l.split()[4] for l in gs[1]}
39+
var_by_name = {reg: BitVec(reg, 1) for reg in regs}
40+
var_by_name |= {f"x{i:02}": Extract(i, i, x) for i in range(46)}
41+
var_by_name |= {f"y{i:02}": Extract(i, i, y) for i in range(46)}
42+
43+
# Create all left hand sides once and for all, but keep the targets separate,
44+
# so we can swap them later.
45+
lhs = []
46+
targets = []
47+
str_to_op = {"AND": and_, "OR": or_, "XOR": xor}
48+
49+
for line in gs[1]:
50+
l = line.split()
51+
G.add_edge(l[4], l[0])
52+
G.add_edge(l[4], l[2])
53+
lhs.append(str_to_op[l[1]](var_by_name[l[0]], var_by_name[l[2]]))
54+
targets.append(var_by_name[l[4]])
55+
56+
57+
def check(bit, swapped_targets):
58+
# Is it possible to construct inputs x, y, and z such that x + y = z, and such that
59+
# all gates do what they are supposed to, such that the first j - 1 bits of z match
60+
# those of the corresponding z registers, but still the value of the j'th bit
61+
# of z is different from the value in the register z{j}?
62+
s = Solver()
63+
s.add(x + y == z)
64+
for v, t in zip(lhs, swapped_targets):
65+
s.add(v == t)
66+
for i in range(bit):
67+
s.add(Extract(i, i, z) == var_by_name[f"z{i:02}"])
68+
s.add(Extract(bit, bit, z) != var_by_name[f"z{bit:02}"])
69+
return s.check()
70+
71+
72+
fixes = set()
73+
for bit in range(45):
74+
# Is there an error on bit j?
75+
if check(bit, targets) == sat:
76+
# Try all swaps. Favor changes that are closer to the broken bit; this isn't
77+
# strictly necessary but does speed the whole thing up a lot.
78+
regs_by_dist = [t for l in nx.bfs_layers(G, f"z{bit:02}") for t in l if t in regs]
79+
for s1, s2 in combinations(regs_by_dist, 2):
80+
t1 = var_by_name[s1]
81+
t2 = var_by_name[s2]
82+
swapped_targets = [t2 if t == t1 else t1 if t == t2 else t for t in targets]
83+
if check(bit, swapped_targets) == unsat:
84+
# Swapping s1 and s2 did the job. Keep the swapped targets.
85+
fixes |= {s1, s2}
86+
targets = swapped_targets
87+
break
88+
89+
print(",".join(sorted(fixes)))

0 commit comments

Comments
 (0)