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 4f193df..3370bed 100644 --- a/src/AoC_2024/Dazbo's_Advent_of_Code_2024.ipynb +++ b/src/AoC_2024/Dazbo's_Advent_of_Code_2024.ipynb @@ -64,7 +64,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 17, "metadata": { "id": "p5Ki_HvOJUWk", "tags": [] @@ -254,7 +254,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 20, "metadata": { "id": "lwP0r3BAaxjt", "tags": [] @@ -327,7 +327,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 21, "metadata": { "id": "Y6nbd6WMryWi", "tags": [] @@ -355,7 +355,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 22, "metadata": { "id": "A8sU4Ez_bBKl", "tags": [] @@ -525,7 +525,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 23, "metadata": { "id": "DT5FSYliC9wp", "tags": [] @@ -7863,7 +7863,7 @@ "\n", "We need to start with all shortest combinations, because we don't know which one will be optimal for the next remote.\n", "\n", - "But now, to get from these directions to the next remote, we need new mappings for each pair, e.g.\n", + "Now, to get from these directions from the next remote we need new mappings for each pair, e.g.\n", "\n", "```text\n", "A -> >: vA\n", @@ -7874,35 +7874,178 @@ "^ -> >: >vA, v>A\n", "```\n", "\n", - "Now, these mappings are repeatable onto the next remote, and the remote after that. So we can definitely cache these and reuse them between robots.\n", + "These mappings are repeatable onto the next remote, and the remote after that. So we can definitely cache these and reuse them between robots.\n", "\n", - "Now if we require a movement like `2 -> 9`, we can see that:\n", + "An example: if we're at `2` and we want to press `9`, these are the keypresses that are required on each upstream keypad:\n", "\n", "```text\n", - ">^AvAA<^A>A>^AvA^A^A^A>AAvA^AA>^AAAvA<^A>A\n", - "AvA<^AA>A^A\n", - "^A>^^AvvvA\n", - "29A\n", + "Start\n", + "Numberic keypad: (2) 9 \n", + "Direction remote 1: (A) > ^ ^ A \n", + "Direction remote 2: (A) v A < ^ A A > A \n", + "Direction remote 3: (A) ^A ^A >A A vA ^A \n", "```\n", "\n", - "```text\n", - "Numeric presses: (2) 9\n", - "Direction 1 presses: > ^ ^ A\n", - "```\n", + "- So we can map all `numeric keypad` pairs to `direction remote 1` presses. \n", + "- And we can map all `remote n` pairs to `remote n+1` presses.\n", + "- All upstream mappings will end in an `A`, i.e. the button press on the remote that causes the button press on the previous.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "class KeypadMapping():\n", + " POINTS_FOR_DIRECTIONS = { \"^\": Point(0, -1), \n", + " \"v\": Point(0, 1), \n", + " \"<\": Point(-1, 0), \n", + " \">\": Point(1, 0) }\n", + " \n", + " DIRECTIONS_FOR_POINTS = {v: k for k, v in POINTS_FOR_DIRECTIONS.items()}\n", + " \n", + " def __init__(self, keypad: list[list[str]]):\n", + " self._keypad = keypad\n", + " self._width = len(keypad[0])\n", + " self._height = len(keypad)\n", + " self._point_to_button: dict[Point, str] = {} # E.g. {P(0,0): '7', P(1,0): '8',..., P(2,3): 'A'}\n", + " self._button_to_point: dict[str, Point] = {} # E.g. {'7': P(0,0), '8': P(1,0), ..., 'A': P(2,3)}\n", + " self._build_keypad_dict()\n", + " self._paths_for_pair = self._compute_paths_for_pair()\n", "\n", - "- Each of these combinations will result in combinations from _direction remote 3_, and so on.\n", - "- So ultimately, we can map each successive keypress - which is the movement from one location to the next - to the final keypresses.\n", - "\n" + " def _build_keypad_dict(self):\n", + " \"\"\" Build a dictionary of keypad points and their associated keys. \"\"\"\n", + " \n", + " for r, row in enumerate(self._keypad):\n", + " for c, key in enumerate(row):\n", + " if key:\n", + " self._point_to_button[Point(c, r)] = key\n", + " self._button_to_point[key] = Point(c, r)\n", + " \n", + " def _compute_paths_for_pair(self):\n", + " \"\"\" Precompute shortest set of paths of directions that take us from one point to all others.\n", + " E.g. {('7', '6'): {'>>vA', '>v>A', 'v>>A'}, ... }\n", + " \"\"\" \n", + " paths = {} # We will build our map, e\n", + " for start in self._button_to_point: # E.g. 7\n", + " for end in self._button_to_point: # E.g. 6\n", + " if start == end:\n", + " paths[start, end] = { \"A\" } # No need to move from a point to itself\n", + " continue # Go to next end\n", + " \n", + " # Now BFS to get all paths from start to end\n", + " all_paths = []\n", + " queue = deque([(self._button_to_point[start], [])]) # E.g. P(2,3), []\n", + " best_path_len = self._width + self._height + 1 # start with a high value\n", + " while queue:\n", + " current, path = queue.popleft() # E.g. P(2,3), []\n", + " \n", + " for direction, point in self.POINTS_FOR_DIRECTIONS.items(): # E.g. '^', P(0, -1)\n", + " new_point = current + point # adjacent point\n", + " if new_point not in self._point_to_button: # Avoids None\n", + " continue\n", + " \n", + " new_path = path + [direction] # E.g. ['^']\n", + " if self._point_to_button[new_point] == end: # We've reached the end of the path\n", + " if best_path_len < len(new_path): # If we've already found a shorter path\n", + " break # And break from outer loop\n", + " \n", + " # Otherwise we've found a new (or same length) best path\n", + " best_path_len = len(new_path)\n", + " all_paths.append(new_path) # Add best (or same length) path\n", + " else:\n", + " queue.append((new_point, new_path))\n", + " else: # only executed if the inner loop did NOT break\n", + " continue\n", + " \n", + " break # only executed if the inner loop DID break\n", + " \n", + " # Convert path lists to strings ending with \"A\"\n", + " paths[start, end] = {\"\".join(path)+\"A\" for path in all_paths\n", + " if len(path) == best_path_len}\n", + " \n", + " logger.debug(f\"{paths=}\")\n", + " return paths\n", + " \n", + " @cache\n", + " def moves_for_sequence(self, code: str) -> tuple[str]:\n", + " \"\"\" Determine the shortest set of move sequences to get from one button to another. \n", + " E.g. with door code 029A. But remember that we start pointing at A. \n", + " Output looks like ('AvvvA', ... )\n", + " \"\"\"\n", + " \n", + " sequential_paths = []\n", + " for (start, end) in zip(\"A\" + code, code): # E.g. ('A', '0'), ('0', '2'), ('2', '9'), ('9', 'A')\n", + " sequential_paths.append(self._paths_for_pair[start, end])\n", + " \n", + " # Now we need the cartesian product of all the paths, and flattened into single strings\n", + " moves_for_seq = tuple(\"\".join(path) for path in product(*sequential_paths))\n", + " return moves_for_seq\n", + " \n", + " def __str__(self):\n", + " return \"\\n\".join(\"\".join(str(row)) for row in self._keypad)\n", + " \n", + " def __repr__(self):\n", + " return f\"{self.__class__.__name__}({self._keypad})\"" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ - "def solve_part1(data):\n", - " pass" + "def complexity(code: str, seq_len: int) -> int:\n", + " return int(code[:-1]) * seq_len\n", + "\n", + "def solve(data, robot_directional_keypads=2) -> int:\n", + " door_codes = data\n", + " logger.debug(f\"{door_codes=}\")\n", + " \n", + " NUMERIC_KEYPAD = [\n", + " [\"7\", \"8\", \"9\"],\n", + " [\"4\", \"5\", \"6\"],\n", + " [\"1\", \"2\", \"3\"],\n", + " [None, \"0\", \"A\"]]\n", + "\n", + " DIRECTION_KEYPAD = [\n", + " [None, \"^\", \"A\"],\n", + " [\"<\", \"v\", \">\"]]\n", + " \n", + " numeric_keypad = KeypadMapping(NUMERIC_KEYPAD)\n", + " direction_keypad = KeypadMapping(DIRECTION_KEYPAD)\n", + " \n", + " logger.debug(f\"\\n{numeric_keypad}\")\n", + " logger.debug(f\"\\n{direction_keypad}\")\n", + " \n", + " total_complexity = 0\n", + " for door_code in door_codes:\n", + " logger.debug(f\"{door_code=}\")\n", + " moves_for_robot_1 = numeric_keypad.moves_for_sequence(door_code)\n", + " logger.debug(f\"{moves_for_robot_1=}\")\n", + " \n", + " # From now on, all mappings are between direction keypads,\n", + " # so the mappings for direction buttons are always the same\n", + " next = moves_for_robot_1\n", + " \n", + " for _ in range(0, robot_directional_keypads):\n", + " moves_for_next_robot = [] \n", + " for move_seq in next: # E.g. 'AvvvA'\n", + " moves_for_next_robot += direction_keypad.moves_for_sequence(move_seq) \n", + " \n", + " # Each block of input move sequences can give us moves of different lengths\n", + " # We want only the shortest sequences\n", + " min_len = min(map(len, moves_for_next_robot))\n", + " moves_for_next_robot = [move for move in moves_for_next_robot if len(move) == min_len]\n", + " next = moves_for_next_robot\n", + " \n", + " logger.debug(f\"{len(moves_for_next_robot[0])=}\")\n", + " \n", + " total_complexity += complexity(door_code, min_len)\n", + " \n", + " return total_complexity\n", + " " ] }, { @@ -7913,18 +8056,23 @@ "source": [ "%%time\n", "sample_inputs = []\n", - "sample_inputs.append(\"\"\"abcdef\"\"\")\n", - "sample_answers = [\"uvwxyz\"]\n", + "sample_inputs.append(\"\"\"\\\n", + "029A\n", + "980A\n", + "179A\n", + "456A\n", + "379A\"\"\")\n", + "sample_answers = [126384]\n", "\n", "logger.setLevel(logging.DEBUG)\n", "for curr_input, curr_ans in zip(sample_inputs, sample_answers):\n", - " validate(solve_part1(curr_input), curr_ans) # test with sample data\n", + " validate(solve(curr_input.splitlines()), curr_ans) # test with sample data\n", " logger.info(\"Test passed\")\n", "\n", "logger.info(\"All tests passed!\")\n", "\n", "logger.setLevel(logging.INFO)\n", - "soln = solve_part1(input_data)\n", + "soln = solve(input_data)\n", "logger.info(f\"Part 1 soln={soln}\")" ] },