diff --git a/src/AoC_2023/Dazbo's_Advent_of_Code_2023.ipynb b/src/AoC_2023/Dazbo's_Advent_of_Code_2023.ipynb index d1de361..a5d4406 100644 --- a/src/AoC_2023/Dazbo's_Advent_of_Code_2023.ipynb +++ b/src/AoC_2023/Dazbo's_Advent_of_Code_2023.ipynb @@ -680,13 +680,9 @@ " return\n", " \n", " # axes, mkr_size = self._plot_info\n", - " \n", - " # axes.clear()\n", - " # min_x, max_x = -0.5, self.width - 0.5\n", - " # min_y, max_y = -0.5, self.height - 0.5\n", - " # axes.set_xlim(min_x, max_x)\n", - " # axes.set_ylim(max_y, min_y)\n", " \n", + " # plot stuff\n", + " \n", " # # save the plot as a frame; store the frame in-memory, using a BytesIO buffer\n", " # frame = BytesIO()\n", " # plt.savefig(frame, format='png') # save to memory, rather than file\n", @@ -5658,54 +5654,40 @@ " return\n", " \n", " axes, mkr_size = self._plot_info\n", - " axes.clear()\n", - " min_x, max_x = -0.5, self.width - 0.5\n", - " min_y, max_y = -0.5, self.height - 0.5\n", - " axes.set_xlim(min_x, max_x)\n", - " axes.set_ylim(max_y, min_y)\n", - " \n", - " dir_sets = []\n", - " for _ in range(4):\n", - " dir_sets.append(set())\n", + " \n", + " dir_sets = [set() for _ in range(4)]\n", " \n", - " vert_splitters = set()\n", - " horz_splitters = set()\n", - " forw_mirrors = set()\n", - " back_mirrors = set()\n", + " vert_splitters, horz_splitters, forw_mirrors, back_mirrors = set(), set(), set(), set()\n", + " splitter_mappings = {\n", + " '|': vert_splitters, \n", + " '-': horz_splitters, \n", + " '/': forw_mirrors, \n", + " '\\\\': back_mirrors\n", + " }\n", + " \n", + " for row_num, row in enumerate(self.array):\n", + " for char_num, char in enumerate(row):\n", + " point = Point(char_num, row_num)\n", + " if char in splitter_mappings:\n", + " splitter_mappings[char].add(point)\n", " \n", - " # todo: just pull out the infra from the array, rather than the path\n", " for point, dirn in self.path_taken:\n", " value = self.value_at_point(point)\n", - " if value in (\"|\", \"-\", \"/\", \"\\\\\"):\n", - " if value == \"|\":\n", - " vert_splitters.add(point)\n", - " elif value == \"-\":\n", - " horz_splitters.add(point)\n", - " elif value == \"/\":\n", - " forw_mirrors.add(point)\n", - " else:\n", - " back_mirrors.add(point)\n", - " else:\n", + " if value not in splitter_mappings:\n", " for dir_set, arrow in zip(dir_sets, (\"^\", \">\", \"v\", \"<\")):\n", " if LightGrid.VECTORS_TO_ARROWS[dirn] == arrow:\n", " dir_set.add(point)\n", " continue\n", - " \n", - " if vert_splitters:\n", - " x, y = zip(*((point.x, point.y) for point in vert_splitters))\n", - " axes.scatter(x, y, marker=\"|\", s=mkr_size, color=\"blue\")\n", - " \n", - " if horz_splitters:\n", - " x, y = zip(*((point.x, point.y) for point in horz_splitters))\n", - " axes.scatter(x, y, marker=\"x\", s=mkr_size, color=\"blue\")\n", - " \n", - " if forw_mirrors:\n", - " x, y = zip(*((point.x, point.y) for point in forw_mirrors))\n", - " axes.scatter(x, y, marker=\"o\", s=mkr_size, color=\"blue\")\n", + "\n", + " # Batched scatter operations\n", + " for splitter_type, marker, color in [(vert_splitters, '|', 'blue'), \n", + " (horz_splitters, 'x', 'blue'), \n", + " (forw_mirrors, 'o', 'blue'), \n", + " (back_mirrors, '*', 'blue')]:\n", " \n", - " if back_mirrors:\n", - " x, y = zip(*((point.x, point.y) for point in back_mirrors))\n", - " axes.scatter(x, y, marker=\"*\", s=mkr_size, color=\"blue\") \n", + " if splitter_type: # check not empty\n", + " x, y = zip(*((point.x, point.y) for point in splitter_type))\n", + " axes.scatter(x, y, marker=marker, s=mkr_size, color=color) \n", " \n", " for dir_set, arrow in zip(dir_sets, (\"^\", \">\", \"v\", \"<\")):\n", " if dir_set:\n", @@ -5743,7 +5725,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 165, "metadata": {}, "outputs": [], "source": [ @@ -5925,7 +5907,28 @@ "\n", "**My solution:**\n", "\n", - "I think this probably needs to use the [A* algorithm](https://aoc.just2good.co.uk/python/shortest_paths)." + "I found this one pretty tough.\n", + "\n", + "It was pretty obvious from the start that I needed either [Dijkstra’s or A* algorithm][https://aoc.just2good.co.uk/python/shortest_paths] to find the path with the lowest cumulative cost. But the actual implementation took me ages to get right.\n", + "\n", + "So... We need to find a path from a start to a goal, where the cost varies depending on the path taken. As usual for a Dijkstra, we need to use a [priority queue](https://aoc.just2good.co.uk/python/priority_queues). First, we need to define some way to represent the state of our journey through the map. I'll represent state in a tuple that stores `(cost, current position, direction, straight steps taken)`. It's important that `cost` comes first, because our priority queue pops the next state based on the lowest value of the first item in the tuple.\n", + "\n", + "We keep popping next states until we reach the goal. To determine valid next states:\n", + "\n", + "- We create `set` to store states that we've already explored.\n", + "- If we're in our initial position, we don't have a direction yet. So we can try all adjacent squares, and with each, we set `straight steps` to 1.\n", + "- If we're not on the first step, then our valid next moves are left, right, or straight on. We can't go backwards.\n", + " - We can move left relative to our current direction by adding the vector `Point(-dirn.y, dirn.x)`.\n", + " - We can move right relative to our current direction by adding the vector `Point(dirn.y, -dirn.x)`.\n", + " - We can only go straight if `straight steps` is less than `3`.\n", + "- Now we've got our new position and new direction, we can determine the cost of the next move, and add that cost to our current cumulative cost.\n", + "\n", + "Finally, we return the total cumulative cost, when we reach the goal.\n", + "\n", + "And that's basically all there is to it for the Dijkstra solution!\n", + "\n", + "I was interested to see what would happen if I changed to an A* implementation. I did this by creating a `heuristic` variable that stores the combination of `path cost` with the _Manhattan distance_ between start and goal. The idea here is that by using the heuristic, we will favour paths that take us towards the goal. But when I added this, the solution took about twice as long to compute. Oh well... It was worth a try! \n", + "\n" ] }, { @@ -5953,6 +5956,7 @@ " queue = [] # priority queue to store current state\n", " \n", " cost:int = 0 # cumulative cost of all moves, based on value of each entered location\n", + " # heuristic:int = 0 + start.manhattan_distance_from(goal)\n", " current_posn:Point = start\n", " dirn:Optional[Point] = None # Current direction. (None when we start.)\n", " straight_steps:int = 0 # number of steps taken in one direction without turning\n", @@ -5989,11 +5993,12 @@ " \n", " for neighbour, dirn, new_steps in next_states:\n", " if (0 <= neighbour.x < len(grid[0]) and 0 <= neighbour.y < len(grid)):\n", - " \n", - " heapq.heappush(queue, (cost+grid[neighbour.y][neighbour.x], \n", - " neighbour, \n", - " dirn, \n", - " new_steps))\n", + " new_cost = cost + grid[neighbour.y][neighbour.x]\n", + " # heuristic = new_cost + neighbour.manhattan_distance_from(goal)\n", + " heapq.heappush(queue, (new_cost, \n", + " neighbour, \n", + " dirn, \n", + " new_steps))\n", " \n", " raise ValueError(\"No solution found\")" ] @@ -6052,12 +6057,30 @@ "source": [ "### Day 17 Part 2\n", "\n", - "We're upgrading to _ultra crucibles_:\n", + "We're upgrading to _ultra crucibles_. These:\n", "\n", - "- Requires a minimum of 4 blocks before it can turn or stop\n", + "- Require a minimum of 4 blocks before it can turn or stop\n", "- But can move 10 blocks before turning\n", "\n", - "**Directing the ultra crucible from the lava pool to the machine parts factory, what is the least heat loss it can incur?**\n" + "**Directing the ultra crucible from the lava pool to the machine parts factory, what is the least heat loss it can incur?**\n", + "\n", + "**My solution:**\n", + "\n", + "- Change the `max_straight` value from 3 to 10. So I've parameterised this.\n", + "- Add a new parameter called `pre_turn`, which determines how many `max_straight` is required before we're allowed to turn. So now I perform this test before turning left or right:\n", + "\n", + "```python\n", + "if straight_steps >= pre_turn\n", + "```\n", + "\n", + "- Finally, we also need to ensure that we've taken at least four straight steps when we arrive at the goal. So we simply amend our exit condition check like this:\n", + "\n", + "```python\n", + " if current_posn == goal and straight_steps >= pre_turn:\n", + " return cost\n", + "```\n", + "\n", + "And that's it!" ] }, {