From 7ad4906e8d32bd4f1f3f63754fe30d491a9b6af7 Mon Sep 17 00:00:00 2001 From: Dazbo Date: Wed, 18 Dec 2024 11:45:05 +0000 Subject: [PATCH] Doc updates --- .../Dazbo's_Advent_of_Code_2024.ipynb | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 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 56f7f9b..f846d16 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": 13, + "execution_count": 194, "metadata": { "id": "p5Ki_HvOJUWk", "tags": [] @@ -85,7 +85,7 @@ "from collections import Counter, deque, defaultdict\n", "from dataclasses import asdict, dataclass, field\n", "from enum import Enum, auto\n", - "from functools import cache, reduce\n", + "from functools import cache, lru_cache, reduce\n", "from itertools import combinations, count, cycle, permutations, product, groupby\n", "from getpass import getpass\n", "from numbers import Number\n", @@ -6734,22 +6734,31 @@ "\n", "**Simulate the first kilobyte (1024 bytes) falling onto your memory space. Afterward, what is the minimum number of steps needed to reach the exit?**\n", "\n", - "Okay, so this seems like a fairly trivial matter of corrupting the locations of 1024 bytes in our list, and the doing a BFS through the resulting maze. (I've got a bad feeling about part 2!)\n", + "Okay, so this seems like a fairly trivial matter of corrupting the locations of 1024 bytes in our list, and then doing a BFS through the resulting maze. (I've got a bad feeling about part 2!)\n", + "\n", + "We could use BFS, but because we know where we need to get to, [A*](https://aoc.just2good.co.uk/python/shortest_paths#a-algorithm) might be a better approach.\n", + "\n", + "I've created a `MemorySpace` class, which extends my usual `Grid` class. It contains a `get_shortest_path()` method, which simply implements the A* algorithm. A* is nearly identical to [Dijkstra's Algorithm](https://aoc.just2good.co.uk/python/shortest_paths#dijkstras-algorithm), except that instead of simply popping from the queue the path with the least cost so far (which in this case would be the number of steps taken), we also include a _distance heuristic_, i.e. we add a factor that indicates how far we are away from the destination. So in my implementation, I've made the cost the combination of steps taken, and the Manhattan distance from the destination.\n", "\n", - "We could use BFS, but because we know where we need to get to, [A*](https://aoc.just2good.co.uk/python/shortest_paths#dijkstras-algorithm) is probably a better approach." + "And that's it!" ] }, { "cell_type": "code", - "execution_count": 160, + "execution_count": 207, "metadata": {}, "outputs": [], "source": [ "class MemorySpace(Grid):\n", " def __init__(self, grid_array):\n", " super().__init__(grid_array)\n", + " \n", + " @lru_cache(maxsize=1000)\n", + " def _get_manhattan_distance_between(self, point_from: Point, point_to: Point):\n", + " return point_from.manhattan_distance_from(point_to)\n", " \n", - " def a_star_path(self, start: Point, end: Point):\n", + " def get_shortest_path(self, start: Point, end: Point):\n", + " \"\"\" Determine shortest path from start to end, using A* \"\"\"\n", " queue: tuple[int, int, Point] = []\n", " backtrace = {}\n", " backtrace[start] = None\n", @@ -6768,7 +6777,7 @@ " for neighbour in valid_neighbours:\n", " if neighbour not in backtrace:\n", " backtrace[neighbour] = current_posn\n", - " cost = neighbour.manhattan_distance_from(end) + (step_count + 1)\n", + " cost = self._get_manhattan_distance_between(neighbour, end) + (step_count + 1)\n", " heapq.heappush(queue, (cost, step_count+1, neighbour))\n", " \n", " raise ValueError(\"No solution found\")\n" @@ -6776,7 +6785,7 @@ }, { "cell_type": "code", - "execution_count": 161, + "execution_count": 208, "metadata": {}, "outputs": [], "source": [ @@ -6793,7 +6802,7 @@ " \n", " logger.debug(f\"\\n{mem_space}\")\n", " \n", - " step_count, backtrace = mem_space.a_star_path(Point(0,0), Point(grid_length-1, grid_length-1))\n", + " step_count, _ = mem_space.get_shortest_path(Point(0,0), Point(grid_length-1, grid_length-1))\n", " return step_count" ] }, @@ -6867,12 +6876,17 @@ "\n", "We don't need to repeat the search for every byte dropped. We only need to try a new path if the last byte dropped has blocked our current best path. So we can skip the majority of bytes dropped.\n", "\n", - "With this tweak, the solution now runs in under a minute. Still slow, but good enough!" + "With this tweak, the solution now runs in under a minute. Still slow, but good enough!\n", + "\n", + "Trying other quick optimisations:\n", + "\n", + "- I tried caching the Manhattan distance. But this made no noticeable improvement.\n", + "- Depending on how convoluted the path is, A* may hinder rather than help. So I've tried removing the Manhattan distance factor, and just running this as a Dijkstra. This also made no noticeable difference." ] }, { "cell_type": "code", - "execution_count": 167, + "execution_count": 212, "metadata": {}, "outputs": [], "source": [ @@ -6892,8 +6906,9 @@ " \n", " mem_space.set_value_at_point(point, \"#\")\n", " try:\n", - " _, backtrace = mem_space.a_star_path(Point(0,0), Point(grid_length-1, grid_length-1))\n", - " except ValueError:\n", + " _, backtrace = mem_space.get_shortest_path(Point(0,0), Point(grid_length-1, grid_length-1))\n", + " except ValueError as e:\n", + " logger.debug(e)\n", " break # No solution - we've found the offending byte!\n", " \n", " return f\"{point.x},{point.y}\""