|
64 | 64 | },
|
65 | 65 | {
|
66 | 66 | "cell_type": "code",
|
67 |
| - "execution_count": 2, |
| 67 | + "execution_count": 17, |
68 | 68 | "metadata": {
|
69 | 69 | "id": "p5Ki_HvOJUWk",
|
70 | 70 | "tags": []
|
|
254 | 254 | },
|
255 | 255 | {
|
256 | 256 | "cell_type": "code",
|
257 |
| - "execution_count": 5, |
| 257 | + "execution_count": 20, |
258 | 258 | "metadata": {
|
259 | 259 | "id": "lwP0r3BAaxjt",
|
260 | 260 | "tags": []
|
|
327 | 327 | },
|
328 | 328 | {
|
329 | 329 | "cell_type": "code",
|
330 |
| - "execution_count": 6, |
| 330 | + "execution_count": 21, |
331 | 331 | "metadata": {
|
332 | 332 | "id": "Y6nbd6WMryWi",
|
333 | 333 | "tags": []
|
|
355 | 355 | },
|
356 | 356 | {
|
357 | 357 | "cell_type": "code",
|
358 |
| - "execution_count": 7, |
| 358 | + "execution_count": 22, |
359 | 359 | "metadata": {
|
360 | 360 | "id": "A8sU4Ez_bBKl",
|
361 | 361 | "tags": []
|
|
525 | 525 | },
|
526 | 526 | {
|
527 | 527 | "cell_type": "code",
|
528 |
| - "execution_count": 8, |
| 528 | + "execution_count": 23, |
529 | 529 | "metadata": {
|
530 | 530 | "id": "DT5FSYliC9wp",
|
531 | 531 | "tags": []
|
|
7863 | 7863 | "\n",
|
7864 | 7864 | "We need to start with all shortest combinations, because we don't know which one will be optimal for the next remote.\n",
|
7865 | 7865 | "\n",
|
7866 |
| - "But now, to get from these directions to the next remote, we need new mappings for each pair, e.g.\n", |
| 7866 | + "Now, to get from these directions from the next remote we need new mappings for each pair, e.g.\n", |
7867 | 7867 | "\n",
|
7868 | 7868 | "```text\n",
|
7869 | 7869 | "A -> >: vA\n",
|
|
7874 | 7874 | "^ -> >: >vA, v>A\n",
|
7875 | 7875 | "```\n",
|
7876 | 7876 | "\n",
|
7877 |
| - "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", |
| 7877 | + "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", |
7878 | 7878 | "\n",
|
7879 |
| - "Now if we require a movement like `2 -> 9`, we can see that:\n", |
| 7879 | + "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", |
7880 | 7880 | "\n",
|
7881 | 7881 | "```text\n",
|
7882 |
| - "<vA<AA>>^AvAA<^A>A<v<A>>^AvA^A<vA>^A<v<A>^A>AAvA^A<v<A>A>^AAAvA<^A>A\n", |
7883 |
| - "<A>AvA<^AA>A<vAAA>^A\n", |
7884 |
| - "^A>^^AvvvA\n", |
7885 |
| - "29A\n", |
| 7882 | + "Start\n", |
| 7883 | + "Numberic keypad: (2) 9 \n", |
| 7884 | + "Direction remote 1: (A) > ^ ^ A \n", |
| 7885 | + "Direction remote 2: (A) v A < ^ A A > A \n", |
| 7886 | + "Direction remote 3: (A) <vA >^A <v<A >^A >A A vA ^A \n", |
7886 | 7887 | "```\n",
|
7887 | 7888 | "\n",
|
7888 |
| - "```text\n", |
7889 |
| - "Numeric presses: (2) 9\n", |
7890 |
| - "Direction 1 presses: > ^ ^ A\n", |
7891 |
| - "```\n", |
| 7889 | + "- So we can map all `numeric keypad` pairs to `direction remote 1` presses. \n", |
| 7890 | + "- And we can map all `remote n` pairs to `remote n+1` presses.\n", |
| 7891 | + "- 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" |
| 7892 | + ] |
| 7893 | + }, |
| 7894 | + { |
| 7895 | + "cell_type": "code", |
| 7896 | + "execution_count": 26, |
| 7897 | + "metadata": {}, |
| 7898 | + "outputs": [], |
| 7899 | + "source": [ |
| 7900 | + "class KeypadMapping():\n", |
| 7901 | + " POINTS_FOR_DIRECTIONS = { \"^\": Point(0, -1), \n", |
| 7902 | + " \"v\": Point(0, 1), \n", |
| 7903 | + " \"<\": Point(-1, 0), \n", |
| 7904 | + " \">\": Point(1, 0) }\n", |
| 7905 | + " \n", |
| 7906 | + " DIRECTIONS_FOR_POINTS = {v: k for k, v in POINTS_FOR_DIRECTIONS.items()}\n", |
| 7907 | + " \n", |
| 7908 | + " def __init__(self, keypad: list[list[str]]):\n", |
| 7909 | + " self._keypad = keypad\n", |
| 7910 | + " self._width = len(keypad[0])\n", |
| 7911 | + " self._height = len(keypad)\n", |
| 7912 | + " self._point_to_button: dict[Point, str] = {} # E.g. {P(0,0): '7', P(1,0): '8',..., P(2,3): 'A'}\n", |
| 7913 | + " self._button_to_point: dict[str, Point] = {} # E.g. {'7': P(0,0), '8': P(1,0), ..., 'A': P(2,3)}\n", |
| 7914 | + " self._build_keypad_dict()\n", |
| 7915 | + " self._paths_for_pair = self._compute_paths_for_pair()\n", |
7892 | 7916 | "\n",
|
7893 |
| - "- Each of these combinations will result in combinations from _direction remote 3_, and so on.\n", |
7894 |
| - "- So ultimately, we can map each successive keypress - which is the movement from one location to the next - to the final keypresses.\n", |
7895 |
| - "\n" |
| 7917 | + " def _build_keypad_dict(self):\n", |
| 7918 | + " \"\"\" Build a dictionary of keypad points and their associated keys. \"\"\"\n", |
| 7919 | + " \n", |
| 7920 | + " for r, row in enumerate(self._keypad):\n", |
| 7921 | + " for c, key in enumerate(row):\n", |
| 7922 | + " if key:\n", |
| 7923 | + " self._point_to_button[Point(c, r)] = key\n", |
| 7924 | + " self._button_to_point[key] = Point(c, r)\n", |
| 7925 | + " \n", |
| 7926 | + " def _compute_paths_for_pair(self):\n", |
| 7927 | + " \"\"\" Precompute shortest set of paths of directions that take us from one point to all others.\n", |
| 7928 | + " E.g. {('7', '6'): {'>>vA', '>v>A', 'v>>A'}, ... }\n", |
| 7929 | + " \"\"\" \n", |
| 7930 | + " paths = {} # We will build our map, e\n", |
| 7931 | + " for start in self._button_to_point: # E.g. 7\n", |
| 7932 | + " for end in self._button_to_point: # E.g. 6\n", |
| 7933 | + " if start == end:\n", |
| 7934 | + " paths[start, end] = { \"A\" } # No need to move from a point to itself\n", |
| 7935 | + " continue # Go to next end\n", |
| 7936 | + " \n", |
| 7937 | + " # Now BFS to get all paths from start to end\n", |
| 7938 | + " all_paths = []\n", |
| 7939 | + " queue = deque([(self._button_to_point[start], [])]) # E.g. P(2,3), []\n", |
| 7940 | + " best_path_len = self._width + self._height + 1 # start with a high value\n", |
| 7941 | + " while queue:\n", |
| 7942 | + " current, path = queue.popleft() # E.g. P(2,3), []\n", |
| 7943 | + " \n", |
| 7944 | + " for direction, point in self.POINTS_FOR_DIRECTIONS.items(): # E.g. '^', P(0, -1)\n", |
| 7945 | + " new_point = current + point # adjacent point\n", |
| 7946 | + " if new_point not in self._point_to_button: # Avoids None\n", |
| 7947 | + " continue\n", |
| 7948 | + " \n", |
| 7949 | + " new_path = path + [direction] # E.g. ['^']\n", |
| 7950 | + " if self._point_to_button[new_point] == end: # We've reached the end of the path\n", |
| 7951 | + " if best_path_len < len(new_path): # If we've already found a shorter path\n", |
| 7952 | + " break # And break from outer loop\n", |
| 7953 | + " \n", |
| 7954 | + " # Otherwise we've found a new (or same length) best path\n", |
| 7955 | + " best_path_len = len(new_path)\n", |
| 7956 | + " all_paths.append(new_path) # Add best (or same length) path\n", |
| 7957 | + " else:\n", |
| 7958 | + " queue.append((new_point, new_path))\n", |
| 7959 | + " else: # only executed if the inner loop did NOT break\n", |
| 7960 | + " continue\n", |
| 7961 | + " \n", |
| 7962 | + " break # only executed if the inner loop DID break\n", |
| 7963 | + " \n", |
| 7964 | + " # Convert path lists to strings ending with \"A\"\n", |
| 7965 | + " paths[start, end] = {\"\".join(path)+\"A\" for path in all_paths\n", |
| 7966 | + " if len(path) == best_path_len}\n", |
| 7967 | + " \n", |
| 7968 | + " logger.debug(f\"{paths=}\")\n", |
| 7969 | + " return paths\n", |
| 7970 | + " \n", |
| 7971 | + " @cache\n", |
| 7972 | + " def moves_for_sequence(self, code: str) -> tuple[str]:\n", |
| 7973 | + " \"\"\" Determine the shortest set of move sequences to get from one button to another. \n", |
| 7974 | + " E.g. with door code 029A. But remember that we start pointing at A. \n", |
| 7975 | + " Output looks like ('<A^A^^>AvvvA', ... )\n", |
| 7976 | + " \"\"\"\n", |
| 7977 | + " \n", |
| 7978 | + " sequential_paths = []\n", |
| 7979 | + " for (start, end) in zip(\"A\" + code, code): # E.g. ('A', '0'), ('0', '2'), ('2', '9'), ('9', 'A')\n", |
| 7980 | + " sequential_paths.append(self._paths_for_pair[start, end])\n", |
| 7981 | + " \n", |
| 7982 | + " # Now we need the cartesian product of all the paths, and flattened into single strings\n", |
| 7983 | + " moves_for_seq = tuple(\"\".join(path) for path in product(*sequential_paths))\n", |
| 7984 | + " return moves_for_seq\n", |
| 7985 | + " \n", |
| 7986 | + " def __str__(self):\n", |
| 7987 | + " return \"\\n\".join(\"\".join(str(row)) for row in self._keypad)\n", |
| 7988 | + " \n", |
| 7989 | + " def __repr__(self):\n", |
| 7990 | + " return f\"{self.__class__.__name__}({self._keypad})\"" |
7896 | 7991 | ]
|
7897 | 7992 | },
|
7898 | 7993 | {
|
7899 | 7994 | "cell_type": "code",
|
7900 |
| - "execution_count": 12, |
| 7995 | + "execution_count": 27, |
7901 | 7996 | "metadata": {},
|
7902 | 7997 | "outputs": [],
|
7903 | 7998 | "source": [
|
7904 |
| - "def solve_part1(data):\n", |
7905 |
| - " pass" |
| 7999 | + "def complexity(code: str, seq_len: int) -> int:\n", |
| 8000 | + " return int(code[:-1]) * seq_len\n", |
| 8001 | + "\n", |
| 8002 | + "def solve(data, robot_directional_keypads=2) -> int:\n", |
| 8003 | + " door_codes = data\n", |
| 8004 | + " logger.debug(f\"{door_codes=}\")\n", |
| 8005 | + " \n", |
| 8006 | + " NUMERIC_KEYPAD = [\n", |
| 8007 | + " [\"7\", \"8\", \"9\"],\n", |
| 8008 | + " [\"4\", \"5\", \"6\"],\n", |
| 8009 | + " [\"1\", \"2\", \"3\"],\n", |
| 8010 | + " [None, \"0\", \"A\"]]\n", |
| 8011 | + "\n", |
| 8012 | + " DIRECTION_KEYPAD = [\n", |
| 8013 | + " [None, \"^\", \"A\"],\n", |
| 8014 | + " [\"<\", \"v\", \">\"]]\n", |
| 8015 | + " \n", |
| 8016 | + " numeric_keypad = KeypadMapping(NUMERIC_KEYPAD)\n", |
| 8017 | + " direction_keypad = KeypadMapping(DIRECTION_KEYPAD)\n", |
| 8018 | + " \n", |
| 8019 | + " logger.debug(f\"\\n{numeric_keypad}\")\n", |
| 8020 | + " logger.debug(f\"\\n{direction_keypad}\")\n", |
| 8021 | + " \n", |
| 8022 | + " total_complexity = 0\n", |
| 8023 | + " for door_code in door_codes:\n", |
| 8024 | + " logger.debug(f\"{door_code=}\")\n", |
| 8025 | + " moves_for_robot_1 = numeric_keypad.moves_for_sequence(door_code)\n", |
| 8026 | + " logger.debug(f\"{moves_for_robot_1=}\")\n", |
| 8027 | + " \n", |
| 8028 | + " # From now on, all mappings are between direction keypads,\n", |
| 8029 | + " # so the mappings for direction buttons are always the same\n", |
| 8030 | + " next = moves_for_robot_1\n", |
| 8031 | + " \n", |
| 8032 | + " for _ in range(0, robot_directional_keypads):\n", |
| 8033 | + " moves_for_next_robot = [] \n", |
| 8034 | + " for move_seq in next: # E.g. '<A^A^^>AvvvA'\n", |
| 8035 | + " moves_for_next_robot += direction_keypad.moves_for_sequence(move_seq) \n", |
| 8036 | + " \n", |
| 8037 | + " # Each block of input move sequences can give us moves of different lengths\n", |
| 8038 | + " # We want only the shortest sequences\n", |
| 8039 | + " min_len = min(map(len, moves_for_next_robot))\n", |
| 8040 | + " moves_for_next_robot = [move for move in moves_for_next_robot if len(move) == min_len]\n", |
| 8041 | + " next = moves_for_next_robot\n", |
| 8042 | + " \n", |
| 8043 | + " logger.debug(f\"{len(moves_for_next_robot[0])=}\")\n", |
| 8044 | + " \n", |
| 8045 | + " total_complexity += complexity(door_code, min_len)\n", |
| 8046 | + " \n", |
| 8047 | + " return total_complexity\n", |
| 8048 | + " " |
7906 | 8049 | ]
|
7907 | 8050 | },
|
7908 | 8051 | {
|
|
7913 | 8056 | "source": [
|
7914 | 8057 | "%%time\n",
|
7915 | 8058 | "sample_inputs = []\n",
|
7916 |
| - "sample_inputs.append(\"\"\"abcdef\"\"\")\n", |
7917 |
| - "sample_answers = [\"uvwxyz\"]\n", |
| 8059 | + "sample_inputs.append(\"\"\"\\\n", |
| 8060 | + "029A\n", |
| 8061 | + "980A\n", |
| 8062 | + "179A\n", |
| 8063 | + "456A\n", |
| 8064 | + "379A\"\"\")\n", |
| 8065 | + "sample_answers = [126384]\n", |
7918 | 8066 | "\n",
|
7919 | 8067 | "logger.setLevel(logging.DEBUG)\n",
|
7920 | 8068 | "for curr_input, curr_ans in zip(sample_inputs, sample_answers):\n",
|
7921 |
| - " validate(solve_part1(curr_input), curr_ans) # test with sample data\n", |
| 8069 | + " validate(solve(curr_input.splitlines()), curr_ans) # test with sample data\n", |
7922 | 8070 | " logger.info(\"Test passed\")\n",
|
7923 | 8071 | "\n",
|
7924 | 8072 | "logger.info(\"All tests passed!\")\n",
|
7925 | 8073 | "\n",
|
7926 | 8074 | "logger.setLevel(logging.INFO)\n",
|
7927 |
| - "soln = solve_part1(input_data)\n", |
| 8075 | + "soln = solve(input_data)\n", |
7928 | 8076 | "logger.info(f\"Part 1 soln={soln}\")"
|
7929 | 8077 | ]
|
7930 | 8078 | },
|
|
0 commit comments