Skip to content

Commit

Permalink
Finished with full walkthrough of day 24
Browse files Browse the repository at this point in the history
  • Loading branch information
derailed-dash committed Jan 4, 2025
1 parent 9bf1db2 commit 98ff211
Showing 1 changed file with 215 additions and 56 deletions.
271 changes: 215 additions & 56 deletions src/AoC_2024/Dazbo's_Advent_of_Code_2024.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -9606,24 +9612,49 @@
"\n",
"And that's basically it!!\n",
"\n",
"<img src=\"https://aoc.just2good.co.uk/assets/images/2024d24pt2_output.png\" width=\"640px\" alt=\"Day 24 Part 2 output\" />\n",
"\n",
"This runs in about 4s.\n"
"<img src=\"https://aoc.just2good.co.uk/assets/images/2024d24pt2_output.png\" width=\"640px\" alt=\"Day 24 Part 2 output\" />"
]
},
{
"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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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))"
]
},
{
Expand Down

0 comments on commit 98ff211

Please sign in to comment.