From 53d0e5d91aae67d2e90a3518924421748c0fa865 Mon Sep 17 00:00:00 2001 From: Akuli Date: Sun, 5 Jan 2025 12:08:25 +0200 Subject: [PATCH] AoC 2024: day 24 (#545) --- examples/aoc2024/day24/.gitignore | 2 + examples/aoc2024/day24/part1.jou | 140 +++++++ examples/aoc2024/day24/part2.jou | 357 ++++++++++++++++++ examples/aoc2024/day24/sampleinput1.txt | 47 +++ .../aoc2024/day24/sampleinput2-correct.txt | 29 ++ examples/aoc2024/day24/sampleinput2-mixed.txt | 22 ++ 6 files changed, 597 insertions(+) create mode 100644 examples/aoc2024/day24/.gitignore create mode 100644 examples/aoc2024/day24/part1.jou create mode 100644 examples/aoc2024/day24/part2.jou create mode 100644 examples/aoc2024/day24/sampleinput1.txt create mode 100644 examples/aoc2024/day24/sampleinput2-correct.txt create mode 100644 examples/aoc2024/day24/sampleinput2-mixed.txt diff --git a/examples/aoc2024/day24/.gitignore b/examples/aoc2024/day24/.gitignore new file mode 100644 index 00000000..8766ee97 --- /dev/null +++ b/examples/aoc2024/day24/.gitignore @@ -0,0 +1,2 @@ +asd.txt +asd.png diff --git a/examples/aoc2024/day24/part1.jou b/examples/aoc2024/day24/part1.jou new file mode 100644 index 00000000..0156e3f7 --- /dev/null +++ b/examples/aoc2024/day24/part1.jou @@ -0,0 +1,140 @@ +import "stdlib/str.jou" +import "stdlib/ascii.jou" +import "stdlib/io.jou" + + +enum LogicOp: + And + Or + Xor + + +class LogicGate: + inputs: int*[2] # -1 = undefined, otherwise 0 or 1 + output: int* + op: LogicOp + + def run(self) -> None: + if *self->inputs[0] == -1 or *self->inputs[1] == -1: + return + + i1 = (*self->inputs[0] == 1) + i2 = (*self->inputs[1] == 1) + + if self->op == LogicOp::And: + *self->output = (i1 and i2) as int + elif self->op == LogicOp::Or: + *self->output = (i1 or i2) as int + elif self->op == LogicOp::Xor: + *self->output = (i1 != i2) as int + else: + assert False + + +class VariableManager: + variables: int[500] + nvariables: int + varnames: byte[4][500] + + def get_var(self, name: byte*) -> int*: + for i = 0; i < self->nvariables; i++: + if strcmp(name, self->varnames[i]) == 0: + return &self->variables[i] + return NULL + + def get_or_create_var(self, name: byte*) -> int*: + exist = self->get_var(name) + if exist != NULL: + return exist + + assert self->nvariables < 500 + i = self->nvariables++ + + assert strlen(name) == 3 + strcpy(self->varnames[i], name) + self->variables[i] = -1 # uninitialized + + return &self->variables[i] + + def get_result(self) -> long: + # Variables starting with z are named z00, z01, z02, ... + # Loop backwards to get bit order right. + zcount = 0 + for i = 0; i < self->nvariables; i++: + if self->varnames[i][0] == 'z': + zcount++ + + result = 0L + for i = zcount-1; i >= 0; i--: + name: byte[100] + sprintf(name, "z%02d", i) + + v = self->get_var(name) + assert v != NULL + if *v == -1: + return -1 + + result *= 2 + result += *v + + return result + + +def main() -> int: + varmgr = VariableManager{} + + gates: LogicGate[500] + ngates = 0 + + f = fopen("sampleinput1.txt", "r") + assert f != NULL + + line: byte[100] + while fgets(line, sizeof(line) as int, f) != NULL: + trim_ascii_whitespace(line) + if line[0] == '\0': + # end of initial values, start of logic gates + break + + # initial value of variable, e.g. "x01: 1" + assert strlen(line) == 6 + assert line[3] == ':' + line[3] = '\0' + *varmgr.get_or_create_var(line) = atoi(&line[5]) + + input1, op, input2, output: byte[4] + while fscanf(f, "%3s %3s %3s -> %3s\n", input1, op, input2, output) == 4: + if strcmp(op, "AND") == 0: + op_enum = LogicOp::And + elif strcmp(op, "OR") == 0: + op_enum = LogicOp::Or + elif strcmp(op, "XOR") == 0: + op_enum = LogicOp::Xor + else: + assert False + + assert ngates < sizeof(gates)/sizeof(gates[0]) + gates[ngates++] = LogicGate{ + inputs = [varmgr.get_or_create_var(input1), varmgr.get_or_create_var(input2)], + output = varmgr.get_or_create_var(output), + op = op_enum, + } + + # Check that no gate has multiple outputs hooked up to its input + for i = 0; i < ngates; i++: + for k = 0; k < 2; k++: + var = gates[i].inputs[k] + count = 0 + for g = &gates[0]; g < &gates[ngates]; g++: + if g->output == var: + count++ + assert count <= 1 + + while varmgr.get_result() == -1: + for g = &gates[0]; g < &gates[ngates]; g++: + g->run() + + printf("%lld\n", varmgr.get_result()) # Output: 2024 + + fclose(f) + return 0 diff --git a/examples/aoc2024/day24/part2.jou b/examples/aoc2024/day24/part2.jou new file mode 100644 index 00000000..386dd32f --- /dev/null +++ b/examples/aoc2024/day24/part2.jou @@ -0,0 +1,357 @@ +# The following commands can be used to create asd.png, which contains a nice +# picture of the input as a graph: +# +# echo 'digraph G {' > asd.txt +# grep -- '->' sampleinput.txt | awk '{print $1 "->" $5 " [label="$2"]\n"$3"->"$5" [label="$2"]"}' >> asd.txt +# echo '}' >> asd.txt +# dot -Tpng -o asd.png asd.txt + +import "stdlib/str.jou" +import "stdlib/mem.jou" +import "stdlib/ascii.jou" +import "stdlib/io.jou" + + +# The sum calculator consists of 45 adders and outputs 46 bits z00,z01,...,z45. +# Each adder (except the first and last) consists of the following 5 logic gates. +enum GateType: + # Inputs: xnn, ynn (e.g. x05 and y05) + # Result: 1 if inputs are 1,0 or 0,1 + # 0 if inputs are 1,1 or 0,0 + # Purpose: Used for both output and overflow checking + InputXor + + # Inputs: xnn, ynn + # Result: 1 if inputs are 1,1 + # 0 if inputs are anything else + # Purpose: Used to detect overflow + InputAnd + + # Inputs: result of InputXor, overflow/carry bit from previous level + # Result: value of znn (the output bit of x+y) + # Purpose: Calculates the sum of the two numbers + OutputXor + + # Inputs: result of InputXor, overflow/carry bit from previous level + # Result: 1 if we overflow when adding bits 1,0 or 0,1 due to carrying + # 0 if there's no overflow, or the overflow happens due to input 1,1 + # Purpose: Creates intermediate value used when calculating overflow/carry + OverflowAnd + + # Inputs: result of InputAnd, result of OverflowAnd + # Result: 1 if we overflow in any way, due to input 1,1 or carrying + # 0 if there is no overflow + # Purpose: This is the overflow/carry bit sent to the next adder + OverflowOr + +# Special cases: +# +# - First adder doesn't need to handle overflow/carry from previous adders, +# so it only has InputXor and InputAnd. Result of InputXor is used for +# output and result of InputAnd is used for carry. +# +# - Last adder's OverflowOr outputs to the last output bit (z45). In other +# adders, it goes to the next adder's OutputXor and OverflowAnd. + + +class Gate: + inputs: byte[4][2] + output: byte[4] + gtype: GateType + + # Gate is sus, if something seems to be wrong with it and we should try to + # swap its output with others. + sus: bool + + +# By design, every gate uses the inputs somehow. +# We can use this to determine a somewhat reasonable ordering for the gates. +# +# Specifically, if x30 and y30 affect a gate, and inputs x31,y31,x32,y32,... +# don't, we assign it number 30, and then sort by affected values. +def sort_gates(gates: Gate**, ngates: int) -> None: + assert ngates < 250 + sort_values: int[250] + memset(sort_values, 0, sizeof(sort_values)) + + maxlen = 400 + todo: byte[4][400] + done: byte[4][400] + + for inputnum = 0; inputnum < 45; inputnum++: + sprintf(todo[0], "x%02d", inputnum) # e.g. x06 + sprintf(todo[1], "y%02d", inputnum) # e.g. y06 + todo_len = 2 + done_len = 0 + + while todo_len > 0: + name: byte[4] = todo[--todo_len] + assert strlen(name) == 3 + + found = False + for i = 0; i < done_len; i++: + if strcmp(done[i], name) == 0: + found = True + if found: + continue + + for i = 0; i < ngates; i++: + if strcmp(gates[i]->inputs[0], name) == 0 or strcmp(gates[i]->inputs[1], name) == 0: + sort_values[i] = inputnum + 1 # never zero + assert todo_len < maxlen + todo[todo_len++] = gates[i]->output + + assert done_len < maxlen + done[done_len++] = name + + # stupid sort algorithm + while True: + did_something = False + for i = 1; i < ngates; i++: + if sort_values[i-1] > sort_values[i]: + memswap(&sort_values[i-1], &sort_values[i], sizeof(sort_values[0])) + memswap(&gates[i-1], &gates[i], sizeof(gates[0])) + did_something = True + if not did_something: + break + + +def sort_strings(strings: byte**, nstrings: int) -> None: + # stupid sort algorithm + while True: + did_something = False + for i = 1; i < nstrings; i++: + if strcmp(strings[i-1], strings[i]) > 0: + memswap(&strings[i-1], &strings[i], sizeof(strings[0])) + did_something = True + if not did_something: + break + + +# Returns number of errors: 1 if connection is wrong, 0 if it is correct. +# g2g = gate to gate +def check_g2g(from: Gate*, to: Gate*, mark_sus: bool) -> int: + if strcmp(from->output, to->inputs[0]) == 0 or strcmp(from->output, to->inputs[1]) == 0: + return 0 + else: + if mark_sus: + from->sus = True + to->sus = True + return 1 + + +# Returns number of errors: 1 if connection is wrong, 0 if it is correct. +# g2o = gate to output +def check_g2o(from: Gate*, znum: int, mark_sus: bool) -> int: + znn: byte[10] # e.g. z05 + sprintf(znn, "z%02d", znum) + if strcmp(from->output, znn) == 0: + return 0 + else: + if mark_sus: + from->sus = True + return 1 + + +# Determines how badly the sum machine seems to be wired. +# Zero is a machine that is working as expected. +def count_errors(orig_gates: Gate*, ngates: int, mark_sus: bool) -> int: + assert ngates <= 250 + gateptrs: Gate*[250] + for i = 0; i < ngates; i++: + gateptrs[i] = &orig_gates[i] + + sort_gates(gateptrs, ngates) + + gate_arrays: Gate*[45][5] + counts = [0, 0, 0, 0, 0] + for i = 0; i < ngates; i++: + type_index = gateptrs[i]->gtype as int + assert counts[type_index] < 45 + gate_arrays[type_index][counts[type_index]++] = gateptrs[i] + + # With real input, there are 45 adders, but the first adder is simpler than others. + # It only has two gates: InputXor for output, InputAnd for overflow/carry. + num_adders = ngates/5 + 1 + assert counts[GateType::InputXor as int] == num_adders + assert counts[GateType::InputAnd as int] == num_adders + assert counts[GateType::OutputXor as int] == num_adders - 1 + assert counts[GateType::OverflowAnd as int] == num_adders - 1 + assert counts[GateType::OverflowOr as int] == num_adders - 1 + + ixors: Gate** = gate_arrays[GateType::InputXor as int] + iands: Gate** = gate_arrays[GateType::InputAnd as int] + oxors: Gate** = gate_arrays[GateType::OutputXor as int] + oands: Gate** = gate_arrays[GateType::OverflowAnd as int] + oors: Gate** = gate_arrays[GateType::OverflowOr as int] + + error_count = 0 + + # First adder: InputXor for output + error_count += check_g2o(ixors[0], 0, mark_sus) + + # First adder: carry into next OutputXor and OverflowAnd + error_count += check_g2g(iands[0], oxors[0], mark_sus) + error_count += check_g2g(iands[0], oands[0], mark_sus) + + for i = 1; i < num_adders; i++: + # Remaining adders: InputXor goes to OutputXor and OverflowAnd + error_count += check_g2g(ixors[i], oxors[i-1], mark_sus) + error_count += check_g2g(ixors[i], oands[i-1], mark_sus) + + # Remaining adders: InputAnd goes to OverflowOr + error_count += check_g2g(iands[i], oors[i-1], mark_sus) + + # Remaining adders: OutputXor computes the result + error_count += check_g2o(oxors[i-1], i, mark_sus) + + # Remaining adders: OverflowAnd goes to OverflowOr + error_count += check_g2g(oands[i-1], oors[i-1], mark_sus) + + if i != num_adders-1: + # Remaining adders except last: OverflowOr goes to next adder's + # OutputXor and OverflowAnd + error_count += check_g2g(oors[i-1], oxors[i], mark_sus) + error_count += check_g2g(oors[i-1], oands[i], mark_sus) + + # Last adder: OverflowOr goes to last output (z45 with real input) + error_count += check_g2o(oors[num_adders-2], num_adders, mark_sus) + + return error_count + + +def read_gates(filename: byte*, ngates: int*) -> Gate[250]: + gates: Gate[250] + memset(gates, 0, sizeof(gates)) # TODO: why is this needed to prevent compiler warning? + *ngates = 0 + + f = fopen(filename, "r") + assert f != NULL + + line: byte[100] + while fgets(line, sizeof(line) as int, f) != NULL: + # Remove comments, if any. I wrote test file by hand. + if strstr(line, "#") != NULL: + *strstr(line, "#") = '\0' + + trim_ascii_whitespace(line) + + # ignore blanks and comment-only lines + if line[0] == '\0': + continue + + # AoC input files have unnecessary lines at start, e.g. "x01: 1" + if strstr(line, ":") != NULL: + continue + + g: Gate + op: byte[4] + n = sscanf(line, "%3s %3s %3s -> %3s", &g.inputs[0], op, &g.inputs[1], &g.output) + assert n == 4 + + # Inputs that don't come from other gates cannot be connected wrong, so + # the wiring mistakes don't mess this up. + if ( + (g.inputs[0][0] == 'x' and g.inputs[1][0] == 'y') + or (g.inputs[0][0] == 'y' and g.inputs[1][0] == 'x') + ): + # Handles input directly, so it must be InputXor or InputAnd + if strcmp(op, "XOR") == 0: + g.gtype = GateType::InputXor + elif strcmp(op, "AND") == 0: + g.gtype = GateType::InputAnd + else: + assert False + else: + if strcmp(op, "XOR") == 0: + g.gtype = GateType::OutputXor + elif strcmp(op, "AND") == 0: + g.gtype = GateType::OverflowAnd + elif strcmp(op, "OR") == 0: + g.gtype = GateType::OverflowOr + else: + assert False + + assert *ngates < sizeof(gates)/sizeof(gates[0]) + gates[(*ngates)++] = g + + fclose(f) + return gates + + +# Ensures that counting errors returns 0 when there are no errors. +def test_counting() -> None: + ngates: int + gates = read_gates("sampleinput2-correct.txt", &ngates) + assert count_errors(gates, ngates, False) == 0 + + +def fix_order_of_outputs(gates: Gate*, ngates: int) -> None: + while True: + for i = 0; i < ngates; i++: + gates[i].sus = False + num_errors = count_errors(gates, ngates, True) + + if num_errors == 0: + break + + nsus = 0 + for i = 0; i < ngates; i++: + if gates[i].sus: + nsus++ + + # Swap two gates so that error count decreases as much as possible + best_g1: Gate* = NULL + best_g2: Gate* = NULL + best_count = -1 + + for g1 = &gates[0]; g1 < &gates[ngates]; g1++: + if not g1->sus: + continue + + for g2 = &g1[1]; g2 < &gates[ngates]; g2++: + if not g2->sus: + continue + + memswap(&g1->output, &g2->output, sizeof(g1->output)) + count = count_errors(gates, ngates, False) + memswap(&g1->output, &g2->output, sizeof(g1->output)) + + if best_count == -1 or count < best_count: + best_count = count + best_g1 = g1 + best_g2 = g2 + + assert best_g1 != NULL + assert best_g2 != NULL + memswap(&best_g1->output, &best_g2->output, sizeof(best_g1->output)) + + +def main() -> int: + test_counting() + + ngates: int + gates = read_gates("sampleinput2-mixed.txt", &ngates) + + orig_gates = gates + fix_order_of_outputs(gates, ngates) + + bad_outputs: byte*[10] + bad_outputs_len = 0 + for i = 0; i < ngates; i++: + if strcmp(orig_gates[i].output, gates[i].output) != 0: + assert bad_outputs_len < 10 + bad_outputs[bad_outputs_len++] = orig_gates[i].output + + # Output: 4 gates were in the wrong place (2 swaps) + printf("%d gates were in the wrong place (%d swaps)\n", bad_outputs_len, bad_outputs_len / 2) + + # Output: ia1,ia2,ix2,ix3 + sort_strings(bad_outputs, bad_outputs_len) + for i = 0; i < bad_outputs_len; i++: + if i != 0: + printf(",") + printf("%s", bad_outputs[i]) + printf("\n") + + return 0 diff --git a/examples/aoc2024/day24/sampleinput1.txt b/examples/aoc2024/day24/sampleinput1.txt new file mode 100644 index 00000000..94b6eed6 --- /dev/null +++ b/examples/aoc2024/day24/sampleinput1.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/examples/aoc2024/day24/sampleinput2-correct.txt b/examples/aoc2024/day24/sampleinput2-correct.txt new file mode 100644 index 00000000..170963c4 --- /dev/null +++ b/examples/aoc2024/day24/sampleinput2-correct.txt @@ -0,0 +1,29 @@ +# Advent of Code doesn't provide very nice test files for part 2. +# +# This file (written by me) defines a working sum machine. It takes 4-bit +# inputs x00,...,x03 and y00,...,y03, and outputs a 5-bit sum in z00,...,z04. + +# First adder +x00 XOR y00 -> z00 # output +x00 AND y00 -> co0 # co = carry out + +# Second adder +x01 XOR y01 -> ix1 # ix = InputXor +x01 AND y01 -> ia1 # ia = InputAnd +ix1 XOR co0 -> z01 # output +ix1 AND co0 -> oa1 # oa = OverflowAnd +ia1 OR oa1 -> co1 # co = carry out + +# Third adder +x02 XOR y02 -> ix2 +x02 AND y02 -> ia2 +ix2 XOR co1 -> z02 +ix2 AND co1 -> oa2 +ia2 OR oa2 -> co2 + +# Fourth (last) adder +x03 XOR y03 -> ix3 +x03 AND y03 -> ia3 +ix3 XOR co2 -> z03 +ix3 AND co2 -> oa3 +ia3 OR oa3 -> z04 # carry goes into last result bit diff --git a/examples/aoc2024/day24/sampleinput2-mixed.txt b/examples/aoc2024/day24/sampleinput2-mixed.txt new file mode 100644 index 00000000..cd0fc65f --- /dev/null +++ b/examples/aoc2024/day24/sampleinput2-mixed.txt @@ -0,0 +1,22 @@ +# Same as sampleinput2-correct.txt, but two pairs of outputs has been swapped. + +x00 XOR y00 -> z00 +x00 AND y00 -> co0 + +x01 XOR y01 -> ix1 +x01 AND y01 -> ix3 # SWAPPED A +ix1 XOR co0 -> z01 +ix1 AND co0 -> oa1 +ia1 OR oa1 -> co1 + +x02 XOR y02 -> ia2 # SWAPPED B +x02 AND y02 -> ix2 # SWAPPED B +ix2 XOR co1 -> z02 +ix2 AND co1 -> oa2 +ia2 OR oa2 -> co2 + +x03 XOR y03 -> ia1 # SWAPPED A +x03 AND y03 -> ia3 +ix3 XOR co2 -> z03 +ix3 AND co2 -> oa3 +ia3 OR oa3 -> z04