Skip to content

Commit

Permalink
Part 1 done. Part 2 does not yet scale.
Browse files Browse the repository at this point in the history
  • Loading branch information
derailed-dash committed Dec 22, 2024
1 parent 7dd2e1c commit baabab5
Showing 1 changed file with 174 additions and 26 deletions.
200 changes: 174 additions & 26 deletions src/AoC_2024/Dazbo's_Advent_of_Code_2024.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 17,
"metadata": {
"id": "p5Ki_HvOJUWk",
"tags": []
Expand Down Expand Up @@ -254,7 +254,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 20,
"metadata": {
"id": "lwP0r3BAaxjt",
"tags": []
Expand Down Expand Up @@ -327,7 +327,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 21,
"metadata": {
"id": "Y6nbd6WMryWi",
"tags": []
Expand Down Expand Up @@ -355,7 +355,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 22,
"metadata": {
"id": "A8sU4Ez_bBKl",
"tags": []
Expand Down Expand Up @@ -525,7 +525,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 23,
"metadata": {
"id": "DT5FSYliC9wp",
"tags": []
Expand Down Expand Up @@ -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",
Expand All @@ -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",
"<vA<AA>>^AvAA<^A>A<v<A>>^AvA^A<vA>^A<v<A>^A>AAvA^A<v<A>A>^AAAvA<^A>A\n",
"<A>AvA<^AA>A<vAAA>^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) <vA >^A <v<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 ('<A^A^^>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. '<A^A^^>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",
" "
]
},
{
Expand All @@ -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}\")"
]
},
Expand Down

0 comments on commit baabab5

Please sign in to comment.