|
| 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