From 98ff2117ec4924b7a6fc9c32d008d952cd2719fd Mon Sep 17 00:00:00 2001 From: Dazbo Date: Sat, 4 Jan 2025 23:43:55 +0000 Subject: [PATCH] Finished with full walkthrough of day 24 --- .../Dazbo's_Advent_of_Code_2024.ipynb | 271 ++++++++++++++---- 1 file changed, 215 insertions(+), 56 deletions(-) diff --git a/src/AoC_2024/Dazbo's_Advent_of_Code_2024.ipynb b/src/AoC_2024/Dazbo's_Advent_of_Code_2024.ipynb index ab8c353..e0d48f8 100644 --- a/src/AoC_2024/Dazbo's_Advent_of_Code_2024.ipynb +++ b/src/AoC_2024/Dazbo's_Advent_of_Code_2024.ipynb @@ -9071,6 +9071,12 @@ " def __str__(self):\n", " return f\"{self.output_wire} = {self.left_wire} {self.gate} {self.right_wire}\"\n", "\n", + "def get_wire_name(char, num):\n", + " \"\"\" Return the wire name, given prefix and digit.\n", + " E.g. z + 1 -> z01 \n", + " \"\"\"\n", + " return char + str(num).zfill(2)\n", + "\n", "def process_input(data: str) -> tuple[dict[str, int], dict[str, Operation]]:\n", " \"\"\" Input data is in two blocks:\n", " 1. Values for wires\n", @@ -9606,24 +9612,49 @@ "\n", "And that's basically it!!\n", "\n", - "\"Day\n", - "\n", - "This runs in about 4s.\n" + "\"Day" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Utility Functions used in All Part 2 Solutions" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ - "@cache\n", - "def get_wire_name(char, num):\n", - " \"\"\" Return the wire name, given prefix and digit.\n", - " E.g. z + 1 -> z01 \n", + "def swap_ops(ops: dict, op_x: str, op_y: str):\n", + " \"\"\" Swap two operations in the operations dictionary.\n", + " Remember that the operations are stored as a dictionary of { output: Operation }\n", + " and that the Operation also contains the output wire name.\n", + " So we need to create a new Operation with the new output wire name,\n", + " rather than simply swapping the dict values.\n", " \"\"\"\n", - " return char + str(num).zfill(2)\n", - "\n", + " new_op_x = Operation(ops[op_y].left_wire, ops[op_y].right_wire, ops[op_y].gate, op_x)\n", + " new_op_y = Operation(ops[op_x].left_wire, ops[op_x].right_wire, ops[op_x].gate, op_y)\n", + " \n", + " ops[op_x] = new_op_x\n", + " ops[op_y] = new_op_y" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This runs in about 4s:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ "@cache\n", "def make_init_wires(count_x_wires: int) -> dict[str, int]:\n", " \"\"\" Initialise a set of x and y input wires, setting all bits to 0. \"\"\"\n", @@ -9726,9 +9757,9 @@ " \n", " return True\n", "\n", - "def swap_ops(ops: dict, op_x: str, op_y: str):\n", - " \"\"\" Swap two operations in the operations dictionary \"\"\"\n", - " ops[op_x], ops[op_y] = ops[op_y], ops[op_x]\n", + "# def swap_ops(ops: dict, op_x: str, op_y: str):\n", + "# \"\"\" Swap two operations in the operations dictionary \"\"\"\n", + "# ops[op_x], ops[op_y] = ops[op_y], ops[op_x]\n", "\n", "def get_lowest_failure(operations, x_wires_count, start=2) -> int:\n", " \"\"\" Identify which bit positions fail the validation, \n", @@ -9748,8 +9779,8 @@ " y_wires_count = len([wire for wire in wires if wire.startswith(\"y\")])\n", " assert wires_count == y_wires_count, \"There should be same number of x and y wires\"\n", "\n", - " # logger.debug(f\"View unique operations for each z wire:\\n\" \\\n", - " # f\"{pretty_print_z_contributors(wires, operations)}\")\n", + " logger.debug(f\"View unique operations for each z wire:\\n\" \\\n", + " f\"{pretty_print_z_contributors(wires, operations)}\")\n", " \n", " lowest_failure = get_lowest_failure(operations, wires_count)\n", " logger.debug(f\"Initial lowest failures: {lowest_failure}\")\n", @@ -9841,47 +9872,60 @@ "source": [ "#### Validating Rules\n", "\n", - "Okay, that previous approach was truly hideous. Maybe it's easier to work with the pattern we spotted afterall!\n", + "Okay, that was horrible. Maybe it's easier to work with the pattern we spotted afterall!\n", "\n", "Recall the pattern:\n", "\n", "```python\n", - "zn = cn ^ sn # z output - XOR of carry and sum\n", - " cn = ic(n-1) | c(n-1) # OR of two carries\n", - " ic(n-1) = e & f # Intermediate carry\n", - " c(n-1) = x(n-1) & y(n-1) # Carry from x(n-1) and y(n-1)\n", - " sn = xn ^ yn # An XOR of the input values themselves\n", + " zn = sn ^ cn # z output - XOR of intermediate sum and carry\n", + " cn = ic_prev | c_prev # OR of two carries\n", + " ic_prev = a & b # Intermediate carry\n", + " c_prev = x_prev & y_prev # Carry from x(n-1) and y(n-1)\n", + " sn = xn ^ yn # An XOR of the input values themselves\n", "```\n", "\n", - "If we look at the printed output, we can see that `z05` follows the pattern:\n", + "If we look at the printed output, we can see that `z03` follows the pattern:\n", + "\n", + "```python\n", + "z03 = psv XOR wdm\n", + " wdm = qrm OR nss\n", + " qrm = pgc AND fvj\n", + " nss = x02 AND y02\n", + " psv = y03 XOR x03\n", + "```\n", + "\n", + "And `z05` follows the pattern:\n", "\n", "```python\n", "z05 = ffn ^ rdj # z05 - XOR of carry and sum\n", - " ffn = x05 ^ y05 # The XOR of the input values themselves\n", - " x05 = 0\n", - " y05 = 1\n", " rdj = cjp | ptd # OR of two carries\n", " cjp = dvq & rkm # Intermediate carry\n", " ptd = x04 & y04 # Carry from x04 and y04\n", + " ffn = x05 ^ y05 # The XOR of the input values themselves\n", + " x05 = 0\n", + " y05 = 1\n", "```" ] }, { - "cell_type": "code", - "execution_count": 17, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "def get_wire_name(char, num):\n", - " \"\"\" Return the wire name, given prefix and digit.\n", - " E.g. z + 1 -> z01 \n", - " \"\"\"\n", - " return char + str(num).zfill(2)\n", + "#### Recursive Functions to Validate Rules\n", "\n", - "def swap_ops(ops: dict, op_x: str, op_y: str):\n", - " \"\"\" Swap two operations in the operations dictionary \"\"\"\n", - " ops[op_x], ops[op_y] = ops[op_y], ops[op_x]\n", + "Here we use functions to recursively check that the rules for a given `z` output wire are valid. This is faster than computing the expected `z` output for our 45 wires * 8 bit combinations.\n", + "\n", + "But other than that, the logic remains the same. I.e. we use these recursive functions to determine where the lowest failure is. Then we swap all wire pairs and re-check. Once the lowest failure increases, we know we've got a good swap, so we keep it.\n", "\n", + "This approach is much faster than the previous." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ "def verify_sn(wire, num, ops: dict[str, Operation]) -> bool:\n", " \"\"\" Verify: sn = xn ^ yn \"\"\"\n", " if wire not in ops: return False\n", @@ -9893,10 +9937,10 @@ " return sorted([left, right]) == [get_wire_name(\"x\", num), get_wire_name(\"y\", num)]\n", "\n", "def verify_carry(wire, num, ops: dict[str, Operation]):\n", - " \"\"\" Verify: \n", - " cn = ic(n-1) | c(n-1) # OR of two carries\n", - " ic(n-1) = e & f # Intermediate carry\n", - " c(n-1) = x(n-1) & y(n-1) # Carry from x(n-1) and y(n-1)\n", + " \"\"\" Verify:\n", + " cn = ic_prev | c_prev # OR of two carries\n", + " ic_prev = sn_prev & other_prev # Intermediate carry\n", + " c_prev = x_prev & y_prev # Carry from x(n-1) and y(n-1)\n", " \"\"\"\n", " if wire not in ops: return False\n", " \n", @@ -9909,10 +9953,10 @@ " \n", " if wire not in ops: return False\n", " \n", - " return verify_direct_carry(left, num - 1, ops) and verify_int_carry(right, num - 1, ops) or \\\n", - " verify_direct_carry(right, num - 1, ops) and verify_int_carry(left, num - 1, ops)\n", + " return verify_carry_prev(left, num - 1, ops) and verify_int_carry(right, num - 1, ops) or \\\n", + " verify_carry_prev(right, num - 1, ops) and verify_int_carry(left, num - 1, ops)\n", "\n", - "def verify_direct_carry(wire, num, ops: dict[str, Operation]):\n", + "def verify_carry_prev(wire, num, ops: dict[str, Operation]):\n", " if wire not in ops: return False\n", " \n", " left, right = ops[wire].left_wire, ops[wire].right_wire\n", @@ -9930,14 +9974,14 @@ " \n", " return verify_sn(left, num, ops) and verify_carry(right, num, ops) or \\\n", " verify_sn(right, num, ops) and verify_carry(left, num, ops)\n", - "\n", + " \n", "def verify_output_wire(num, ops: dict[str, Operation]) -> bool:\n", " \"\"\" Verify a z wire has been properly created and follows the rules. \n", - " zn = sn ^ cn # z output - XOR of intermediate sum and carry\n", - " cn = ic(n-1) | c(n-1) # OR of two carries\n", - " ic(n-1) = e & f # Intermediate carry\n", - " c(n-1) = x(n-1) & y(n-1) # Carry from x(n-1) and y(n-1)\n", - " sn = xn ^ yn # An XOR of the input values themselves\n", + " zn = sn ^ cn # z output - XOR of intermediate sum and carry\n", + " cn = ic_prev | c_prev # OR of two carries\n", + " ic_prev = sn_prev & other_prev # Intermediate carry\n", + " c_prev = x_prev & y_prev # Carry from x(n-1) and y(n-1)\n", + " sn = xn ^ yn # An XOR of the input values themselves\n", " \"\"\"\n", " wire = get_wire_name(\"z\", num)\n", " if wire not in ops: \n", @@ -9952,18 +9996,16 @@ " return sorted([left, right]) == [\"x00\", \"y00\"]\n", " \n", " # Otherwise recurse\n", - " # zn = sn ^ cn\n", - " # sn = xn ^ yn \n", - " # cn = ic(n-1) | c(n-1)\n", " return verify_sn(left, num, ops) and verify_carry(right, num, ops) or \\\n", " verify_sn(right, num, ops) and verify_carry(left, num, ops)\n", "\n", - "def find_lowest_fail(ops: dict, wires_count: int, start=0):\n", + "def find_lowest_fail(ops: dict, wires_count: int, start=2):\n", " \"\"\" Find the lowest bit position that fails the verification. \n", " Returns -1 if all are valid. \"\"\"\n", " for i in range(start, wires_count):\n", " if not verify_output_wire(i, ops): \n", " return i\n", + " \n", " return -1\n", "\n", "def solve_part2(data: str):\n", @@ -9999,9 +10041,126 @@ " logger.debug(f\"Swap candidates: {swap_candidates}\")\n", "\n", " # Take our swap tuples and flatten into a single list\n", - " swaps = []\n", - " swaps += [item for pair in swap_candidates for item in pair] # extend the list\n", - " return \",\".join(sorted(swaps))\n" + " return \",\".join(sorted(item for pair in swap_candidates for item in pair))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%time\n", + "logger.setLevel(logging.DEBUG)\n", + "soln = solve_part2(input_data)\n", + "logger.info(f\"Part 2 soln={soln}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Validate Rules for n and n-1 Only\n", + "\n", + "If we've previously validated rules for `zn`, then we don't need to recurse all the way down to validate the rules. We can simply validate the rule set of `z(n-1)`. I.e. this set:\n", + "\n", + "```text\n", + "zn = sn ^ cn # z output - XOR of intermediate sum and carry\n", + " cn = ic_prev | c_prev # OR of two carries\n", + " ic_prev = sn_prev & other_prev # Intermediate carry\n", + " c_prev = x_prev & y_prev # Carry from x(n-1) and y(n-1)\n", + " sn = xn ^ yn # An XOR of the input values themselves\n", + "```\n", + "\n", + "Furthermore, with this rule set, we know what values are _expected_ from each rule, and we can compare against the operations we've actually got. Whenever we fail a verification, we can just substitute the value that is expected. So we take the old output wire and the expected output wire, and add them to our list of swaps.\n", + "\n", + "This is super fast!" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "def match_operation(ops: dict[str, Operation], \n", + " gate: str | None = None, \n", + " left: str | None = None, right: str | None = None) -> Operation:\n", + " \"\"\" Find an operation that matches the specified criteria. \n", + " Returns None if no match is found, otherwise the Operation. \"\"\"\n", + " for op in ops.values():\n", + " if gate and op.gate != gate: continue\n", + " if left and left not in (op.left_wire, op.right_wire): continue\n", + " if right and right not in (op.left_wire, op.right_wire): continue\n", + " return op\n", + " \n", + " return None\n", + " \n", + "def verify(num: int, ops: dict[str, Operation]) -> list[str] | None:\n", + " \"\"\" Verify a z wire has been properly created and follows the rules. \n", + " zn = sn ^ cn # z output - XOR of intermediate sum and carry\n", + " cn = ic_prev | c_prev # OR of two carries\n", + " ic_prev = sn_prev & other_prev # Intermediate carry\n", + " c_prev = x_prev & y_prev # Carry from x(n-1) and y(n-1)\n", + " sn = xn ^ yn # An XOR of the input values themselves\n", + " \n", + " Returns None if the verification passes, otherwise a tuple of the two wires to swap.\n", + " \"\"\" \n", + " wire = get_wire_name(\"z\", num)\n", + " if num == 0: # Special case for z00\n", + " if sorted([ops[wire].left_wire, ops[wire].right_wire]) == [\"x00\", \"y00\"]:\n", + " return None\n", + " \n", + " sn_prev = match_operation(ops, gate=\"XOR\", left=get_wire_name(\"x\", num-1), right=get_wire_name(\"y\", num-1))\n", + " ic_prev = match_operation(ops, gate=\"AND\", left=sn_prev.output_wire)\n", + " c_prev = match_operation(ops, gate=\"AND\", left=get_wire_name(\"x\", num-1), right=get_wire_name(\"y\", num-1))\n", + " cn = match_operation(ops, gate=\"OR\", left=ic_prev.output_wire, right=c_prev.output_wire)\n", + " sn = match_operation(ops, gate=\"XOR\", left=get_wire_name(\"x\", num), right=get_wire_name(\"y\", num))\n", + " zn = match_operation(ops, gate=\"XOR\", left=sn.output_wire, right=cn.output_wire)\n", + " \n", + " # Failure scenarios - we need a swap\n", + " if zn is None or zn.output_wire != wire:\n", + " if zn is None:\n", + " # The rule is incorrect for this z wire\n", + " zn = ops[wire] # Get the incorrect operation\n", + " # Find the difference between the two sets of wires\n", + " # I.e. the wires that currently connect to the output wire, and the wires that should\n", + " # E.g. {\"ntr, \"bpt\"} ^ {\"ntr\", \"krj\"} = {\"bpt\", \"krj\"}\n", + " to_swap = set([zn.left_wire, zn.right_wire]) ^ set([sn.output_wire, cn.output_wire])\n", + " if zn.output_wire != wire:\n", + " # The rule is correct but the output wire is wrong\n", + " to_swap = [wire, zn.output_wire] # E.g. wire should be \"z06\" but output_wire is \"fkp\"\n", + " \n", + " return sorted(to_swap)\n", + " \n", + " return None\n", + "\n", + "def find_lowest_fail(ops: dict, wires_count: int, start=2) -> tuple[int, list[str]]:\n", + " \"\"\" Find the lowest bit position that fails the verification. \n", + " Returns the lowest failure position and the wires to swap. \n", + " Returns -1 if all are valid. \"\"\"\n", + " for i in range(start, wires_count):\n", + " swaps = verify(i, ops)\n", + " if swaps:\n", + " return i, swaps\n", + " \n", + " return -1, None\n", + "\n", + "def solve_part2(data: str):\n", + " wires, operations = process_input(data)\n", + " wires_count = len([wire for wire in wires if wire.startswith(\"x\")])\n", + " \n", + " swap_candidates = []\n", + " lowest_failure, swaps = find_lowest_fail(operations, wires_count) # E.g. 6 for z06\n", + " while swaps:\n", + " logger.debug(f\"Lowest failure: {lowest_failure}\")\n", + " swap_candidates.append(swaps)\n", + " swap_ops(operations, *swaps)\n", + " lowest_failure, swaps = find_lowest_fail(operations, wires_count, start=lowest_failure)\n", + " logger.debug(f\"Swap candidates: {swap_candidates}\")\n", + "\n", + " # Take our swap tuples and flatten into a single list\n", + " return \",\".join(sorted(item for pair in swap_candidates for item in pair))" ] }, {