diff --git a/README.md b/README.md index 4f94d05..0403c2c 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ years 2022, 2023, 2024 (ongoing). They are all coded in Java, needing Java 21 at * [2024-12-21](https://adventofcode.com/2024/day/21): [solution](src/advent2024/Puzzle21.java) (navigating multiple levels of keypads). * [2024-12-22](https://adventofcode.com/2024/day/22): [solution](src/advent2024/Puzzle22.java) (best sequence of differences in parallel pseudorandom streams). * [2024-12-23](https://adventofcode.com/2024/day/23): [solution](src/advent2024/Puzzle23.java) (largest clique in a graph). +* [2024-12-24](https://adventofcode.com/2024/day/24): [solution](src/advent2024/Puzzle24.java) (fixing an adder circuit by swapping wires). * [2024-12-25](https://adventofcode.com/2024/day/25): [solution](src/advent2024/Puzzle25.java) (trivial problem with measuring column heights). # Acknowledgements diff --git a/src/advent2024/Puzzle24.java b/src/advent2024/Puzzle24.java index b7d6be8..974b64d 100644 --- a/src/advent2024/Puzzle24.java +++ b/src/advent2024/Puzzle24.java @@ -3,31 +3,33 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap; -import static java.util.Map.entry; +import static com.google.common.collect.Iterables.getOnlyElement; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSetMultimap; -import com.google.common.collect.SetMultimap; +import com.google.common.graph.Graph; +import com.google.common.graph.GraphBuilder; +import com.google.common.graph.Graphs; +import com.google.common.graph.MutableGraph; import com.google.common.io.CharStreams; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collections; import java.util.Deque; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.NavigableMap; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.IntStream; @@ -36,7 +38,6 @@ /** * @author Éamonn McManus */ -// PARTIAL SOLUTION for Part 2. This still involved some manual steps. public class Puzzle24 { private static final String SAMPLE1 = """ @@ -119,395 +120,247 @@ public static void main(String[] args) throws Exception { List lines = CharStreams.readLines(r); int blank = lines.indexOf(""); checkArgument(blank > 0); - ImmutableMap initialWireValues = - parseInitialWireValues(lines.subList(0, blank)); - ImmutableList gates = parseGates(lines.subList(blank + 1, lines.size())); - ImmutableSetMultimap wireToInputs = - gates.stream() - .flatMap( - gate -> - Stream.of(entry(gate.inputs.get(0), gate), entry(gate.inputs.get(1), gate))) - .collect(toImmutableSetMultimap(Map.Entry::getKey, Map.Entry::getValue)); + InitialWireValues wireValues = parseInitialWireValues(lines.subList(0, blank)); + Circuit circuit = parseCircuit(lines.subList(blank + 1, lines.size())); System.out.printf( - "For %s, computed number is %d\n", name, compute(wireToInputs, initialWireValues)); + "For %s, computed number is %d\n", name, circuit.compute(wireValues.x, wireValues.y)); if (name.equals("problem")) { - part2(wireToInputs); + part2(circuit); } } } } - private static long compute( - SetMultimap wireToInputs, ImmutableMap initialWireValues) { - NavigableMap wireValues = new TreeMap<>(); - Deque> queue = new ArrayDeque<>(initialWireValues.entrySet()); - while (!queue.isEmpty()) { - var value = queue.remove(); - var wire = value.getKey(); - var bit = value.getValue(); - var old = wireValues.put(wire, bit); - if (old != null) { - return -1; - } - for (Gate gate : wireToInputs.get(wire)) { - Boolean input0 = wireValues.get(gate.inputs.get(0)); - Boolean input1 = wireValues.get(gate.inputs.get(1)); - if (input0 != null && input1 != null) { - var outputValue = gate.op.op(input0, input1); - queue.add(entry(gate.output, outputValue)); + // This was a very hard problem, as befits Day 24. Like many people, I found the correct answer + // through a combination of coding and manual inspection. By printing a textual representation of + // each output zNN, I could see that there is a "correct" version for each bit. For example, bit 2 + // looks like this: + // ((((x00 AND y00) AND (x01 XOR y01)) OR (x01 AND y01)) XOR (x02 XOR y02)) + // The general pattern for all bits other than the first and last is + // carryBit XOR addBit + // In the example, carryBit is (((x00 AND y00) AND (x01 XOR y01)) OR (x01 AND y01)), and + // addBit is (x02 OR y02). Then carryBit has a recursive structure, which is + // carry(bit) = (carry(bit - 1) AND (x[bit - 1] XOR y[bit - 1])) OR (x[bit - 1] AND y[bit - 1]) + // This means that there is a carry into `bit` if there was a carry into `bit - 1` and exactly one + // of the `bit - 1` inputs was 1, or if both of the `bit - 1` inputs were 1. (That's not the only + // way to write an adder stage, but it is correct and is used consistently.) + // + // That insight was enough for me to be able to figure out manually what the exchanges needed to + // be, and I initially stopped at that point. But I was dissatisfied with the manual element, and + // later went on to automate it. + // + // Since the tree leading to bit i should only depend on bits i and earlier, we can start from the + // lowest bits and check for each one whether the shape of the tree corresponds to the shape + // expected. If not, we can simply try every possible exchange of outputs until we do find the + // shape we want. There are 222 gates, so that's only 24,531 echanges. + // + // I spent a lot of time trying other approaches before hitting on this. I tried inspecting the + // results of additions and making exchanges to see if I could improve them. If you determine that + // the result of 11000 + 01000 is incorrect, you could try exchanges until you get the correct + // answer, and maybe the correct answer for every xx000 + yy000, but it's not certain that an + // exchange that gets the correct answer there is actually the right exchange. + // I also tried looking for the largest correct subtree of the expected tree, but that didn't + // prove helpful because it isn't necessarily the case that that the root of that subtree needs to + // have its output wire exchanged. + private static void part2(Circuit circuit) { + MutableGraph actualGraph = circuit.graph; + List operations = + actualGraph.nodes().stream() + .filter(n -> n instanceof Operation) + .map(n -> (Operation) n) + .toList(); + Set exchangedOutputs = new TreeSet<>(); + for (int bit = 0; bit < circuit.outputs.size() && exchangedOutputs.size() < 8; bit++) { + MutableGraph expectedGraph = GraphBuilder.directed().build(); + Operation expected = expected(bit, expectedGraph); + Node actual = getOnlyElement(actualGraph.predecessors(circuit.outputs.get(bit))); + if (!isomorphic(expected, expectedGraph, actual, actualGraph)) { + boolean found = false; + exchanges: + for (int aIndex = 0; aIndex < operations.size(); aIndex++) { + Operation a = operations.get(aIndex); + Set aOuts = new LinkedHashSet<>(actualGraph.successors(a)); + for (int bIndex = aIndex + 1; bIndex < operations.size(); bIndex++) { + Operation b = operations.get(bIndex); + Set bOuts = new LinkedHashSet<>(actualGraph.successors(b)); + if (aOuts.contains(b) || bOuts.contains(a) || !Collections.disjoint(aOuts, bOuts)) { + continue; + } + exchangeOutputs(actualGraph, a, b); + if (!Graphs.hasCycle(actualGraph)) { + Node newActual = getOnlyElement(actualGraph.predecessors(circuit.outputs.get(bit))); + if (isomorphic(expected, expectedGraph, newActual, actualGraph)) { + exchangedOutputs.add(a.originalOut); + exchangedOutputs.add(b.originalOut); + found = true; + break exchanges; + } + } + exchangeOutputs(actualGraph, a, b); + } + } + if (!found) { + System.out.printf("Could not fix incorrect bit %d\n", bit); + break; } } } - return wireValues.tailMap("z00").entrySet().stream() - .mapToLong( - e -> { - int bit = Integer.parseInt(e.getKey().substring(1)); - checkState(bit >= 0 && bit < 64); - return e.getValue() ? 1L << bit : 0; - }) - .reduce(0L, (a, b) -> a | b); + System.out.printf("List of exchanged outputs is %s\n", String.join(",", exchangedOutputs)); } - private static String bitToWire(String prefix, int bit) { - if (bit >= 10) { - return prefix + bit; - } else { - return prefix + "0" + bit; + private static void exchangeOutputs(MutableGraph graph, Node a, Node b) { + Set aOuts = new LinkedHashSet<>(graph.successors(a)); + Set bOuts = new LinkedHashSet<>(graph.successors(b)); + for (Node n : aOuts) { + graph.removeEdge(a, n); + graph.putEdge(b, n); + } + for (Node n : bOuts) { + graph.removeEdge(b, n); + graph.putEdge(a, n); } } - private static final ImmutableSet X_INPUTS = - IntStream.range(0, 45).mapToObj(i -> bitToWire("x", i)).collect(toImmutableSet()); - private static final ImmutableSet Y_INPUTS = - IntStream.range(0, 45).mapToObj(i -> bitToWire("y", i)).collect(toImmutableSet()); - private static final ImmutableMap ZERO_INITIAL_WIRE_VALUES = - Stream.concat(X_INPUTS.stream(), Y_INPUTS.stream()) - .collect(toImmutableMap(s -> s, s -> false)); - - private sealed interface Node {} - - private record Intermediate(Node a, Op op, Node b) implements Node { - @Override - public String toString() { - List operands = List.of(a.toString(), b.toString()); - if (operands.get(0).compareTo(operands.get(1)) > 0) { - operands = List.of(operands.get(1), operands.get(0)); - } - return "(" + operands.get(0) + " " + op + " " + operands.get(1) + ")"; + // Add to `graph` the tree of nodes that we would expect to see as the output for the given bit. + // For example if bit=2, this is what we would expect to see connected to z02. + private static Operation expected(int bit, MutableGraph graph) { + if (bit == 0) { + return operation(new Terminal("x00"), Op.XOR, new Terminal("y00"), graph); } - - @Override - public boolean equals(Object x) { - return x instanceof Intermediate that - && this.op == that.op - && ((this.a.equals(that.a) && this.b.equals(that.b)) - || (this.a.equals(that.b) && this.b.equals(that.a))); + Operation carry = carry(bit, graph); + if (bit == 45) { + return carry; } + Operation addBit = operation(new Terminal("x", bit), Op.XOR, new Terminal("y", bit), graph); + return operation(carry, Op.XOR, addBit, graph); + } - @Override - public int hashCode() { - return 1001 * op.hashCode() + 31 * (a.hashCode() + b.hashCode()); + private static Operation carry(int intoBit, MutableGraph graph) { + if (intoBit == 1) { + return inputOperation(0, Op.AND, graph); } + int prevBit = intoBit - 1; + Operation prevCarry = carry(prevBit, graph); + Operation prevWasZero = inputOperation(prevBit, Op.XOR, graph); + Operation carryFromPrev = operation(prevCarry, Op.AND, prevWasZero, graph); + Operation prevBothOnes = inputOperation(prevBit, Op.AND, graph); + return operation(carryFromPrev, Op.OR, prevBothOnes, graph); } - private record Input(String wire) implements Node { - Input(String prefix, int bit) { - this(bitToWire(prefix, bit)); - } + private static final AtomicInteger operationId = new AtomicInteger(); - @Override - public String toString() { - return wire; - } + private static Operation operation(Node lhs, Op op, Node rhs, MutableGraph graph) { + Operation operation = new Operation(op, "op" + operationId.getAndIncrement()); + graph.putEdge(lhs, operation); + graph.putEdge(rhs, operation); + return operation; } - // Work in progress. I figured out what the tree for each output bit should be, and then searched - // for subtrees of that tree. The output wire of the largest subtree needs to be swapped with - // the output wire of another tree. I worked out which one manually. I think it's probably - // possible to complete the automation but I am stopping here for now. - private static void part2(ImmutableSetMultimap wireToInputs) { - for (int i = 0; i < 4; i++) { - System.out.printf("%d: %s\n", i, expected(i)); - } - ImmutableMap exchanges = - ImmutableMap.of( - "z07", "vmv", - "vmv", "z07", - "z20", "kfm", - "kfm", "z20", - "hnv", "z28", - "z28", "hnv", - "tqr", "hth", - "hth", "tqr"); - wireToInputs = - wireToInputs.entries().stream() - .map( - e -> { - Gate gate1 = e.getValue(); - String output1 = gate1.output; - String output2 = exchanges.get(output1); - if (output2 == null) { - return e; - } else { - Gate gate2 = new Gate(gate1.inputs, gate1.op, output2); - return entry(e.getKey(), gate2); - } - }) - .collect(toImmutableSetMultimap(Map.Entry::getKey, Map.Entry::getValue)); - ImmutableMap outputWireToGate = - wireToInputs.values().stream().distinct().collect(toImmutableMap(g -> g.output, g -> g)); - ImmutableMap outputWireToNode = - outputWireToGate.entrySet().stream() - .map(e -> entry(e.getKey(), makeNode(e.getValue(), outputWireToGate))) - .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); - // This is the manual step: I put in each output bit that had the wrong tree of gates, looked - // at the subtrees found below, and worked out what swaps were needed. Kind of laborious. - Node expected1 = expected(35); - System.out.printf("expected: %s\n", expected1); - findSubtrees(expected1, outputWireToNode); - for (int z = 0; z < 46; z++) { - String wire = bitToWire("z", z); - Node expected = expected(z); - Node actual = makeNode(outputWireToGate.get(wire), outputWireToGate); - if (!expected.equals(actual)) { - System.out.printf("Mismatch for %s:\n", wire); - System.out.printf("Expected: %s\n", expected); - System.out.printf("Actual : %s\n", actual); - } - } - System.out.println(String.join(",", new TreeSet<>(exchanges.keySet()))); + private static Operation inputOperation(int bit, Op op, MutableGraph graph) { + return operation(new Terminal("x", bit), op, new Terminal("y", bit), graph); } - private static void findSubtrees(Node expected, ImmutableMap outputWireToNode) { - switch (expected) { - case Input i -> { - return; - } - case Intermediate(Node a, Op op, Node b) -> { - for (var entry : outputWireToNode.entrySet()) { - if (entry.getValue().equals(expected)) { - System.out.printf("found %s = %s\n", entry.getKey(), expected); - return; - } - } - findSubtrees(a, outputWireToNode); - findSubtrees(b, outputWireToNode); - } - } - } + private sealed interface Node {} - private static Node expected(int bit) { - if (bit == 0) { - return new Intermediate(new Input("x00"), Op.XOR, new Input("y00")); - } - Node addBit = new Intermediate(new Input("x", bit), Op.XOR, new Input("y", bit)); - Node carry = carry(bit); - if (bit == 45) { - return carry; - } else { - return new Intermediate(carry, Op.XOR, addBit); + private record Terminal(String name) implements Node { + Terminal(String prefix, int i) { + this(i < 10 ? (prefix + "0" + i) : (prefix + i)); } } - private static Node carry(int intoBit) { - if (intoBit == 1) { - return new Intermediate(new Input("x00"), Op.AND, new Input("y00")); + private record Operation(Op op, String originalOut) implements Node {} + + private record Circuit( + ImmutableList xInputs, + ImmutableList yInputs, + ImmutableList outputs, + MutableGraph graph) { + Circuit { + checkArgument(xInputs.size() == yInputs.size()); // simplifies things slightly } - int prevBit = intoBit - 1; - Node prevCarry = carry(prevBit); - Node prevWasZero = new Intermediate(new Input("x", prevBit), Op.XOR, new Input("y", prevBit)); - Node carryFromPrev = new Intermediate(prevCarry, Op.AND, prevWasZero); - Node prevBothOnes = new Intermediate(new Input("x", prevBit), Op.AND, new Input("y", prevBit)); - return new Intermediate(carryFromPrev, Op.OR, prevBothOnes); - } - private static Node makeNode(Gate gate, ImmutableMap wireToOutput) { - List inputs = new ArrayList<>(); - for (String wire : gate.inputs) { - Gate inGate = wireToOutput.get(wire); - Node in = (inGate == null) ? new Input(wire) : makeNode(inGate, wireToOutput); - inputs.add(in); + String toString(Node node) { + return treeString(graph, node); } - return new Intermediate(inputs.get(0), gate.op, inputs.get(1)); - } - // My idea here was to check each bit separately to see whether addition of just that bit - // works correctly. That should limit the number of gates whose outputs we need to consider - // swapping. Unfortunately it doesn't limit it enough. - // I also tried swapping each pair of gates and seeing whether that reduced the number of wrong - // additions, but that isn't sound because a swap can reduce that number coincidentally without - // being a correct swap. - private static void oldPart2(ImmutableSetMultimap wireToInputs) { - checkState(!wireToInputs.containsKey("x45")); - checkState(!wireToInputs.containsKey("y45")); - Set suspectWires = new TreeSet<>(); - Set suspectOutputs = new TreeSet<>(); - for (int shift = 0; shift < 44; shift++) { - for (long x = 0; x < 2; x++) { - for (long y = 0; y < 2; y++) { - var xyInputs = - ImmutableMap.of( - bitToWire("x", shift), (x & 1) == 1, - bitToWire("y", shift), (y & 1) == 1); - var initialWireValues = - ImmutableMap.builder() - .putAll(ZERO_INITIAL_WIRE_VALUES) - .putAll(xyInputs) - .buildKeepingLast(); - long expected = (x << shift) + (y << shift); - long actual = compute(wireToInputs, initialWireValues); - if (expected != actual) { - System.out.printf( - "Differed for %x + %x, expected %x, actual %x\n", - x << shift, y << shift, expected, actual); - suspectWires.addAll(xyInputs.keySet()); - long thisBit = 1L << shift; - if ((expected & thisBit) != (actual & thisBit)) { - suspectOutputs.add(bitToWire("z", shift)); - } - long nextBit = 1L << (shift + 1); - if ((expected & nextBit) != (actual & nextBit)) { - suspectOutputs.add(bitToWire("z", shift + 1)); + long compute(long x, long y) { + Map nodeValues = new LinkedHashMap<>(); + record NodeValue(Node node, boolean value) {} + Deque queue = new ArrayDeque<>(); + for (int i = 0; i < xInputs.size(); i++) { + queue.add(new NodeValue(xInputs.get(i), (x & (1L << i)) != 0)); + queue.add(new NodeValue(yInputs.get(i), (y & (1L << i)) != 0)); + } + while (!queue.isEmpty()) { + var nodeValue = queue.remove(); + Node node = nodeValue.node; + boolean value = nodeValue.value; + nodeValues.put(node, value); + // Propagate values through the graph. When we dequeue a node and its value, we see if the + // successors of that node now have known values. If a successor is an output terminal then + // it does, and also if it is an operation both of whose inputs are now known. + for (Node output : graph.successors(node)) { + switch (output) { + case Terminal t -> queue.add(new NodeValue(t, value)); + case Operation(Op op, String unused) -> { + List inputs = + graph.predecessors(output).stream().map(nodeValues::get).toList(); + checkState(inputs.size() == 2); + if (!inputs.contains(null)) { + boolean computedValue = op.op(inputs.get(0), inputs.get(1)); + queue.add(new NodeValue(output, computedValue)); + } } } } } - } - System.out.printf("%d suspect wires: %s\n", suspectWires.size(), suspectWires); - System.out.printf("%d suspect outputs: %s\n", suspectOutputs.size(), suspectOutputs); - Set suspectGates = new LinkedHashSet<>(); - for (String wire : suspectWires) { - suspectGates.addAll(gatesReached(wire, wireToInputs)); - } - System.out.printf( - "%d suspect gates from suspect wires: %s\n", suspectGates.size(), suspectGates); - suspectGates.clear(); - for (String wire : suspectOutputs) { - suspectGates.addAll(gatesLeadingTo(wire, wireToInputs)); - } - System.out.printf( - "%d suspect gates from suspect result wires: %s\n", suspectGates.size(), suspectGates); - for (Gate gate : new LinkedHashSet<>(wireToInputs.values())) { - if (new LinkedHashSet<>(gate.inputs).equals(Set.of("x07", "y07"))) { - System.out.println(gate); - } - } - ImmutableSetMultimap modifiedWireToInputs = wireToInputs; - long currentIncorrect = incorrectCount(modifiedWireToInputs); - outer: - while (currentIncorrect > 0) { - System.out.printf("@@@ looping with currentIncorrect=%d\n", currentIncorrect); - ImmutableMap wireToOutput = - modifiedWireToInputs.values().stream() - .distinct() - .collect(toImmutableMap(g -> g.output, g -> g)); - ImmutableList wires = wireToOutput.keySet().asList(); - for (int i = 0; i < wires.size(); i++) { - String wire1 = wires.get(i); - System.out.printf("%d of %d (%s)\n", i, wires.size(), wires.get(i)); - for (int j = i + 1; j < wires.size(); j++) { - String wire2 = wires.get(j); - Gate gate1 = wireToOutput.get(wire1); - Gate gate2 = wireToOutput.get(wire2); - Gate newGate1 = new Gate(gate1.inputs, gate1.op, gate2.output); - Gate newGate2 = new Gate(gate2.inputs, gate2.op, gate1.output); - ImmutableSetMultimap newWireToInputs = - modifiedWireToInputs.entries().stream() - .map( - e -> - entry( - e.getKey(), - e.getValue() == gate1 - ? newGate1 - : e.getValue() == gate2 ? newGate2 : e.getValue())) - .collect(toImmutableSetMultimap(Map.Entry::getKey, Map.Entry::getValue)); - long newIncorrect = incorrectCount(newWireToInputs); - if (newIncorrect < currentIncorrect) { - System.out.printf("@@@ swapped %s and %s\n", newGate1, newGate2); - currentIncorrect = newIncorrect; - modifiedWireToInputs = newWireToInputs; - continue outer; - } + long z = 0; + for (int i = 0; i < outputs.size(); i++) { + // This implicitly checks that every zNN terminal has received a value; otherwise NPE. + if (nodeValues.get(outputs.get(i))) { + z |= 1L << i; } } - throw new AssertionError("Did not progress from currentIncorrect=" + currentIncorrect); + return z; } } - private static long incorrectCount(SetMultimap wireToInputs) { - long count = 0; - for (int shift = 0; shift < 44; shift++) { - for (long x = 0; x < 2; x++) { - for (long y = 0; y < 2; y++) { - var xyInputs = - ImmutableMap.of( - bitToWire("x", shift), (x & 1) == 1, - bitToWire("y", shift), (y & 1) == 1); - var initialWireValues = - ImmutableMap.builder() - .putAll(ZERO_INITIAL_WIRE_VALUES) - .putAll(xyInputs) - .buildKeepingLast(); - long expected = (x << shift) + (y << shift); - long actual = compute(wireToInputs, initialWireValues); - if (expected != actual) { - long thisBit = 1L << shift; - if ((expected & thisBit) != (actual & thisBit)) { - count++; - } - long nextBit = 1L << (shift + 1); - if ((expected & nextBit) != (actual & nextBit)) { - count++; - } - } + // Determines if the tree at a is isomorphic to the tree at b. Checking whether graphs are + // isomorphic is a hard problem in general, but when the graphs are binary trees it is easy. + static boolean isomorphic(Node a, Graph aGraph, Node b, Graph bGraph) { + return switch (a) { + case Terminal(String name) -> { + // We don't need to support comparing zNN terminal nodes, so we don't. + checkState(aGraph.predecessors(a).isEmpty()); + yield a.equals(b); + } + case Operation aOp -> { + if (!(b instanceof Operation bOp) || aOp.op != bOp.op) { + yield false; } + var aIn = new ArrayList<>(aGraph.predecessors(a)); + var bIn = new ArrayList<>(bGraph.predecessors(b)); + checkState(aIn.size() == 2 && bIn.size() == 2); + var a0 = aIn.get(0); + var a1 = aIn.get(1); + var b0 = bIn.get(0); + var b1 = bIn.get(1); + yield (isomorphic(a0, aGraph, b0, bGraph) && isomorphic(a1, aGraph, b1, bGraph)) + || (isomorphic(a0, aGraph, b1, bGraph) && isomorphic(a1, aGraph, b0, bGraph)); } - } - return count; - } - - private static ImmutableSet gatesLeadingTo( - String endWire, ImmutableSetMultimap wireToInputs) { - ImmutableMap wireToOutput = - wireToInputs.values().stream().distinct().collect(toImmutableMap(g -> g.output, g -> g)); - Set gatesLeadingTo = new LinkedHashSet<>(); - gatesLeadingTo(gatesLeadingTo, endWire, wireToOutput); - return ImmutableSet.copyOf(gatesLeadingTo); + }; } - private static void gatesLeadingTo( - Set gatesLeadingTo, String endWire, ImmutableMap wireToOutput) { - Gate gate = wireToOutput.get(endWire); - if (gate != null) { - gatesLeadingTo.add(gate); - gatesLeadingTo(gatesLeadingTo, gate.inputs.get(0), wireToOutput); - gatesLeadingTo(gatesLeadingTo, gate.inputs.get(1), wireToOutput); - } - } - - private static ImmutableSet gatesReached( - String startWire, ImmutableSetMultimap wireToInputs) { - Set gatesReached = new LinkedHashSet<>(); - gatesReached(gatesReached, Set.of(startWire), wireToInputs); - return ImmutableSet.copyOf(gatesReached); - } - - private static void gatesReached( - Set gatesReached, - Set toVisit, - ImmutableSetMultimap wireToInputs) { - if (toVisit.isEmpty()) { - return; - } - Set newToVisit = new TreeSet<>(); - for (String wire : toVisit) { - for (Gate gate : wireToInputs.get(wire)) { - if (gatesReached.add(gate)) { - newToVisit.add(gate.output); - } + private static String treeString(Graph graph, Node node) { + return switch (node) { + case Terminal(String name) -> name; + case Operation(Op op, var unused) -> { + List operands = + graph.predecessors(node).stream().map(n -> treeString(graph, n)).sorted().toList(); + checkState(operands.size() == 2); + yield "(" + operands.get(0) + " " + op + " " + operands.get(1) + ")"; } - } - gatesReached(gatesReached, newToVisit, wireToInputs); + }; } private enum Op { @@ -545,16 +398,95 @@ static Gate parse(String s) { } } - private static ImmutableList parseGates(List lines) { - return lines.stream().map(Gate::parse).collect(toImmutableList()); + private static Circuit parseCircuit(List lines) { + List gates = lines.stream().map(Gate::parse).toList(); + // Assumes we have the same number of xNN and yNN inputs. + int biggestInput = + gates.stream() + .flatMap(g -> g.inputs.stream()) + .filter(s -> s.startsWith("x")) + .mapToInt(s -> Integer.parseInt(s.substring(1))) + .max() + .getAsInt(); + int biggestOutput = + gates.stream() + .map(Gate::output) + .filter(s -> s.startsWith("z")) + .mapToInt(s -> Integer.parseInt(s.substring(1))) + .max() + .getAsInt(); + ImmutableList xInputs = + IntStream.rangeClosed(0, biggestInput) + .mapToObj(i -> new Terminal("x", i)) + .collect(toImmutableList()); + ImmutableList yInputs = + IntStream.rangeClosed(0, biggestInput) + .mapToObj(i -> new Terminal("y", i)) + .collect(toImmutableList()); + ImmutableList zOutputs = + IntStream.rangeClosed(0, biggestOutput) + .mapToObj(i -> new Terminal("z", i)) + .collect(toImmutableList()); + + // Maps from a wire name to the unique node that has that wire as its output. + Map wireToNode = new TreeMap<>(); + + MutableGraph graph = GraphBuilder.directed().build(); + + // Add the input terminals to the graph. + Stream.concat(xInputs.stream(), yInputs.stream()) + .forEach( + t -> { + wireToNode.put(t.name, t); + graph.addNode(t); + }); + + // Add the gates to the graph. If a gate is connected to an output terminal (zNN), connect the + // corresponding graph nodes. + for (Gate gate : gates) { + Operation operation = new Operation(gate.op, gate.output); + wireToNode.put(gate.output, operation); + graph.addNode(operation); + if (gate.output.startsWith("z")) { + int i = Integer.parseInt(gate.output.substring(1)); + graph.putEdge(operation, zOutputs.get(i)); + } + } + + // Connect the inputs of each gate. + for (Gate gate : gates) { + Node output = wireToNode.get(gate.output); + for (String input : gate.inputs) { + graph.putEdge(wireToNode.get(input), output); + } + } + + return new Circuit(xInputs, yInputs, zOutputs, graph); } private static final Pattern WIRE_VALUE_PATTERN = Pattern.compile("(\\w+): ([01])"); - private static ImmutableMap parseInitialWireValues(List lines) { - return lines.stream() - .map(WIRE_VALUE_PATTERN::matcher) - .peek(Matcher::matches) - .collect(toImmutableMap(m -> m.group(1), m -> m.group(2).equals("1"))); + record InitialWireValues(long x, long y) {} + + private static InitialWireValues parseInitialWireValues(List lines) { + ImmutableSet ones = + lines.stream() + .map(WIRE_VALUE_PATTERN::matcher) + .peek(Matcher::matches) + .filter(m -> m.group(2).equals("1")) + .map(m -> m.group(1)) + .collect(toImmutableSet()); + long x = 0; + long y = 0; + for (var one : ones) { + int i = Integer.parseInt(one.substring(1)); + long bit = 1L << i; + if (one.startsWith("x")) { + x |= bit; + } else { + y |= bit; + } + } + return new InitialWireValues(x, y); } } \ No newline at end of file diff --git a/test/advent2024/PuzzleResultsTest.java b/test/advent2024/PuzzleResultsTest.java index 1878e24..9f472af 100644 --- a/test/advent2024/PuzzleResultsTest.java +++ b/test/advent2024/PuzzleResultsTest.java @@ -144,8 +144,7 @@ public class PuzzleResultsTest { For Part 2 big sample, sum is 9021 For Part 1 problem, sum is 1463512 For Part 2 problem, sum is 1486520 - """ - ), + """), entry( Puzzle16.class, """ @@ -217,6 +216,14 @@ public class PuzzleResultsTest { For problem, number of 3-cliques is 1485 For problem, largest clique is cc,dz,ea,hj,if,it,kf,qo,sk,ug,ut,uv,wh """), + entry( + Puzzle24.class, + """ + For sample 1, computed number is 4 + For sample 2, computed number is 2024 + For problem, computed number is 52956035802096 + List of exchanged outputs is hnv,hth,kfm,tqr,vmv,z07,z20,z28 + """), entry( Puzzle25.class, """