From e949ce99323a2af27d3a273a6eaf92f9cffa16f7 Mon Sep 17 00:00:00 2001 From: Bert Peters Date: Tue, 24 Dec 2024 20:56:12 +0100 Subject: [PATCH] Sort of functional implementation of 2024 day 24 --- 2024/bonus/24todot.py | 15 +++++ 2024/src/aoc/days/day24.py | 114 +++++++++++++++++++++++++++++++++++++ 2024/tests/samples/24.txt | 47 +++++++++++++++ 2024/tests/test_day24.py | 7 +++ 4 files changed, 183 insertions(+) create mode 100644 2024/bonus/24todot.py create mode 100644 2024/src/aoc/days/day24.py create mode 100644 2024/tests/samples/24.txt create mode 100644 2024/tests/test_day24.py diff --git a/2024/bonus/24todot.py b/2024/bonus/24todot.py new file mode 100644 index 0000000..ea61975 --- /dev/null +++ b/2024/bonus/24todot.py @@ -0,0 +1,15 @@ +import fileinput + +print("digraph day24 {") + +for line in fileinput.input(): + parts = line.split(" ") + if len(parts) != 5: + continue + + first, op, second, _, result = parts + print(f'{first}{second}{op} [label="{op}"];') + print(f"{first} -> {first}{second}{op} -> {result};") + print(f"{second} -> {first}{second}{op};") + +print("}") diff --git a/2024/src/aoc/days/day24.py b/2024/src/aoc/days/day24.py new file mode 100644 index 0000000..43ae560 --- /dev/null +++ b/2024/src/aoc/days/day24.py @@ -0,0 +1,114 @@ +import functools +import re + +from . import SeparateRunner + + +def parse_input(input: str) -> tuple[dict[str, int], dict[str, tuple[str, str, str]]]: + variable_part, rules_part = input.strip().split("\n\n") + + variables = {} + + for line in variable_part.splitlines(): + variable, value = line.split(": ") + variables[variable] = int(value) + + rules = {} + + for first, op, second, result in re.findall( + r"(\w+) (XOR|OR|AND) (\w+) -> (\w+)", rules_part + ): + rules[result] = (first, op, second) + + return variables, rules + + +class DayRunner(SeparateRunner): + @classmethod + def part1(cls, input: str) -> int: + variables, rules = parse_input(input) + + @functools.cache + def get_value(variable: str) -> int: + if variable in variables: + return variables[variable] + + first, op, second = rules[variable] + first_v = get_value(first) + second_v = get_value(second) + + match op: + case "AND": + return first_v & second_v + case "OR": + return first_v | second_v + case "XOR": + return first_v ^ second_v + + result = 0 + for variable in reversed(sorted(rules)): + if not variable.startswith("z"): + continue + result = result * 2 + get_value(variable) + + return result + + @classmethod + def part2(cls, input: str) -> str: + variables, rules = parse_input(input) + + max_bit = int( + max(variable for variable in rules if variable.startswith("z"))[1:] + ) + + def find_invalid(output: str, pattern) -> set[str]: + if pattern is None: + return set() + + if output in rules: + left, op, right = rules[output] + elif output == pattern: + return set() + else: + return {output} + + pop, pleft, pright = pattern + + if op != pop: + return {output} + + wrong_normal = find_invalid(left, pleft) | find_invalid(right, pright) + wrong_mirror = find_invalid(left, pright) | find_invalid(right, pleft) + + least_wrong = min(wrong_mirror, wrong_normal, key=len) + + return least_wrong + + # First one is a half adder, that's a simple pattern + invalid = find_invalid("z00", ["XOR", "x00", "y00"]) + # Second one is missing a reference to the before-previous adder, so it's a + # slightly different patterns + invalid |= find_invalid( + "z01", ["XOR", ["AND", "x00", "y00"], ["XOR", "x01", "y01"]] + ) + + for n in range(2, max_bit): + xcurr = f"x{n:02}" + ycurr = f"y{n:02}" + zcurr = f"z{n:02}" + xprev = f"x{n-1:02}" + yprev = f"y{n-1:02}" + + invalid |= find_invalid( + zcurr, + [ + "XOR", + ["XOR", xcurr, ycurr], + ["OR", ["AND", xprev, yprev], ["AND", ["XOR", xprev, yprev], None]], + ], + ) + + # This code somehow believes `ktp` is invalid, but it's fine on closer + # inspection. Will figure that out later. + + return ",".join(sorted(invalid)) diff --git a/2024/tests/samples/24.txt b/2024/tests/samples/24.txt new file mode 100644 index 0000000..94b6eed --- /dev/null +++ b/2024/tests/samples/24.txt @@ -0,0 +1,47 @@ +x00: 1 +x01: 0 +x02: 1 +x03: 1 +x04: 0 +y00: 1 +y01: 1 +y02: 1 +y03: 1 +y04: 1 + +ntg XOR fgs -> mjb +y02 OR x01 -> tnw +kwq OR kpj -> z05 +x00 OR x03 -> fst +tgd XOR rvg -> z01 +vdt OR tnw -> bfw +bfw AND frj -> z10 +ffh OR nrd -> bqk +y00 AND y03 -> djm +y03 OR y00 -> psh +bqk OR frj -> z08 +tnw OR fst -> frj +gnj AND tgd -> z11 +bfw XOR mjb -> z00 +x03 OR x00 -> vdt +gnj AND wpb -> z02 +x04 AND y00 -> kjc +djm OR pbm -> qhw +nrd AND vdt -> hwm +kjc AND fst -> rvg +y04 OR y02 -> fgs +y01 AND x02 -> pbm +ntg OR kjc -> kwq +psh XOR fgs -> tgd +qhw XOR tgd -> z09 +pbm OR djm -> kpj +x03 XOR y03 -> ffh +x00 XOR y04 -> ntg +bfw OR bqk -> z06 +nrd XOR fgs -> wpb +frj XOR qhw -> z04 +bqk OR frj -> z07 +y03 OR x01 -> nrd +hwm AND bqk -> z03 +tgd XOR rvg -> z12 +tnw OR pbm -> gnj diff --git a/2024/tests/test_day24.py b/2024/tests/test_day24.py new file mode 100644 index 0000000..119c818 --- /dev/null +++ b/2024/tests/test_day24.py @@ -0,0 +1,7 @@ +from aoc.days.day24 import DayRunner + +from . import get_data + + +def test_sample_part1() -> None: + assert DayRunner.part1(get_data(24)) == 2024