|
680 | 680 | " return\n",
|
681 | 681 | " \n",
|
682 | 682 | " # axes, mkr_size = self._plot_info\n",
|
683 |
| - " \n", |
684 |
| - " # axes.clear()\n", |
685 |
| - " # min_x, max_x = -0.5, self.width - 0.5\n", |
686 |
| - " # min_y, max_y = -0.5, self.height - 0.5\n", |
687 |
| - " # axes.set_xlim(min_x, max_x)\n", |
688 |
| - " # axes.set_ylim(max_y, min_y)\n", |
689 | 683 | " \n",
|
| 684 | + " # plot stuff\n", |
| 685 | + " \n", |
690 | 686 | " # # save the plot as a frame; store the frame in-memory, using a BytesIO buffer\n",
|
691 | 687 | " # frame = BytesIO()\n",
|
692 | 688 | " # plt.savefig(frame, format='png') # save to memory, rather than file\n",
|
|
5658 | 5654 | " return\n",
|
5659 | 5655 | " \n",
|
5660 | 5656 | " axes, mkr_size = self._plot_info\n",
|
5661 |
| - " axes.clear()\n", |
5662 |
| - " min_x, max_x = -0.5, self.width - 0.5\n", |
5663 |
| - " min_y, max_y = -0.5, self.height - 0.5\n", |
5664 |
| - " axes.set_xlim(min_x, max_x)\n", |
5665 |
| - " axes.set_ylim(max_y, min_y)\n", |
5666 |
| - " \n", |
5667 |
| - " dir_sets = []\n", |
5668 |
| - " for _ in range(4):\n", |
5669 |
| - " dir_sets.append(set())\n", |
| 5657 | + " \n", |
| 5658 | + " dir_sets = [set() for _ in range(4)]\n", |
5670 | 5659 | " \n",
|
5671 |
| - " vert_splitters = set()\n", |
5672 |
| - " horz_splitters = set()\n", |
5673 |
| - " forw_mirrors = set()\n", |
5674 |
| - " back_mirrors = set()\n", |
| 5660 | + " vert_splitters, horz_splitters, forw_mirrors, back_mirrors = set(), set(), set(), set()\n", |
| 5661 | + " splitter_mappings = {\n", |
| 5662 | + " '|': vert_splitters, \n", |
| 5663 | + " '-': horz_splitters, \n", |
| 5664 | + " '/': forw_mirrors, \n", |
| 5665 | + " '\\\\': back_mirrors\n", |
| 5666 | + " }\n", |
| 5667 | + " \n", |
| 5668 | + " for row_num, row in enumerate(self.array):\n", |
| 5669 | + " for char_num, char in enumerate(row):\n", |
| 5670 | + " point = Point(char_num, row_num)\n", |
| 5671 | + " if char in splitter_mappings:\n", |
| 5672 | + " splitter_mappings[char].add(point)\n", |
5675 | 5673 | " \n",
|
5676 |
| - " # todo: just pull out the infra from the array, rather than the path\n", |
5677 | 5674 | " for point, dirn in self.path_taken:\n",
|
5678 | 5675 | " value = self.value_at_point(point)\n",
|
5679 |
| - " if value in (\"|\", \"-\", \"/\", \"\\\\\"):\n", |
5680 |
| - " if value == \"|\":\n", |
5681 |
| - " vert_splitters.add(point)\n", |
5682 |
| - " elif value == \"-\":\n", |
5683 |
| - " horz_splitters.add(point)\n", |
5684 |
| - " elif value == \"/\":\n", |
5685 |
| - " forw_mirrors.add(point)\n", |
5686 |
| - " else:\n", |
5687 |
| - " back_mirrors.add(point)\n", |
5688 |
| - " else:\n", |
| 5676 | + " if value not in splitter_mappings:\n", |
5689 | 5677 | " for dir_set, arrow in zip(dir_sets, (\"^\", \">\", \"v\", \"<\")):\n",
|
5690 | 5678 | " if LightGrid.VECTORS_TO_ARROWS[dirn] == arrow:\n",
|
5691 | 5679 | " dir_set.add(point)\n",
|
5692 | 5680 | " continue\n",
|
5693 |
| - " \n", |
5694 |
| - " if vert_splitters:\n", |
5695 |
| - " x, y = zip(*((point.x, point.y) for point in vert_splitters))\n", |
5696 |
| - " axes.scatter(x, y, marker=\"|\", s=mkr_size, color=\"blue\")\n", |
5697 |
| - " \n", |
5698 |
| - " if horz_splitters:\n", |
5699 |
| - " x, y = zip(*((point.x, point.y) for point in horz_splitters))\n", |
5700 |
| - " axes.scatter(x, y, marker=\"x\", s=mkr_size, color=\"blue\")\n", |
5701 |
| - " \n", |
5702 |
| - " if forw_mirrors:\n", |
5703 |
| - " x, y = zip(*((point.x, point.y) for point in forw_mirrors))\n", |
5704 |
| - " axes.scatter(x, y, marker=\"o\", s=mkr_size, color=\"blue\")\n", |
| 5681 | + "\n", |
| 5682 | + " # Batched scatter operations\n", |
| 5683 | + " for splitter_type, marker, color in [(vert_splitters, '|', 'blue'), \n", |
| 5684 | + " (horz_splitters, 'x', 'blue'), \n", |
| 5685 | + " (forw_mirrors, 'o', 'blue'), \n", |
| 5686 | + " (back_mirrors, '*', 'blue')]:\n", |
5705 | 5687 | " \n",
|
5706 |
| - " if back_mirrors:\n", |
5707 |
| - " x, y = zip(*((point.x, point.y) for point in back_mirrors))\n", |
5708 |
| - " axes.scatter(x, y, marker=\"*\", s=mkr_size, color=\"blue\") \n", |
| 5688 | + " if splitter_type: # check not empty\n", |
| 5689 | + " x, y = zip(*((point.x, point.y) for point in splitter_type))\n", |
| 5690 | + " axes.scatter(x, y, marker=marker, s=mkr_size, color=color) \n", |
5709 | 5691 | " \n",
|
5710 | 5692 | " for dir_set, arrow in zip(dir_sets, (\"^\", \">\", \"v\", \"<\")):\n",
|
5711 | 5693 | " if dir_set:\n",
|
|
5743 | 5725 | },
|
5744 | 5726 | {
|
5745 | 5727 | "cell_type": "code",
|
5746 |
| - "execution_count": null, |
| 5728 | + "execution_count": 165, |
5747 | 5729 | "metadata": {},
|
5748 | 5730 | "outputs": [],
|
5749 | 5731 | "source": [
|
|
5925 | 5907 | "\n",
|
5926 | 5908 | "**My solution:**\n",
|
5927 | 5909 | "\n",
|
5928 |
| - "I think this probably needs to use the [A* algorithm](https://aoc.just2good.co.uk/python/shortest_paths)." |
| 5910 | + "I found this one pretty tough.\n", |
| 5911 | + "\n", |
| 5912 | + "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", |
| 5913 | + "\n", |
| 5914 | + "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", |
| 5915 | + "\n", |
| 5916 | + "We keep popping next states until we reach the goal. To determine valid next states:\n", |
| 5917 | + "\n", |
| 5918 | + "- We create `set` to store states that we've already explored.\n", |
| 5919 | + "- 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", |
| 5920 | + "- 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", |
| 5921 | + " - We can move left relative to our current direction by adding the vector `Point(-dirn.y, dirn.x)`.\n", |
| 5922 | + " - We can move right relative to our current direction by adding the vector `Point(dirn.y, -dirn.x)`.\n", |
| 5923 | + " - We can only go straight if `straight steps` is less than `3`.\n", |
| 5924 | + "- 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", |
| 5925 | + "\n", |
| 5926 | + "Finally, we return the total cumulative cost, when we reach the goal.\n", |
| 5927 | + "\n", |
| 5928 | + "And that's basically all there is to it for the Dijkstra solution!\n", |
| 5929 | + "\n", |
| 5930 | + "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", |
| 5931 | + "\n" |
5929 | 5932 | ]
|
5930 | 5933 | },
|
5931 | 5934 | {
|
|
5953 | 5956 | " queue = [] # priority queue to store current state\n",
|
5954 | 5957 | " \n",
|
5955 | 5958 | " cost:int = 0 # cumulative cost of all moves, based on value of each entered location\n",
|
| 5959 | + " # heuristic:int = 0 + start.manhattan_distance_from(goal)\n", |
5956 | 5960 | " current_posn:Point = start\n",
|
5957 | 5961 | " dirn:Optional[Point] = None # Current direction. (None when we start.)\n",
|
5958 | 5962 | " straight_steps:int = 0 # number of steps taken in one direction without turning\n",
|
|
5989 | 5993 | " \n",
|
5990 | 5994 | " for neighbour, dirn, new_steps in next_states:\n",
|
5991 | 5995 | " if (0 <= neighbour.x < len(grid[0]) and 0 <= neighbour.y < len(grid)):\n",
|
5992 |
| - " \n", |
5993 |
| - " heapq.heappush(queue, (cost+grid[neighbour.y][neighbour.x], \n", |
5994 |
| - " neighbour, \n", |
5995 |
| - " dirn, \n", |
5996 |
| - " new_steps))\n", |
| 5996 | + " new_cost = cost + grid[neighbour.y][neighbour.x]\n", |
| 5997 | + " # heuristic = new_cost + neighbour.manhattan_distance_from(goal)\n", |
| 5998 | + " heapq.heappush(queue, (new_cost, \n", |
| 5999 | + " neighbour, \n", |
| 6000 | + " dirn, \n", |
| 6001 | + " new_steps))\n", |
5997 | 6002 | " \n",
|
5998 | 6003 | " raise ValueError(\"No solution found\")"
|
5999 | 6004 | ]
|
|
6052 | 6057 | "source": [
|
6053 | 6058 | "### Day 17 Part 2\n",
|
6054 | 6059 | "\n",
|
6055 |
| - "We're upgrading to _ultra crucibles_:\n", |
| 6060 | + "We're upgrading to _ultra crucibles_. These:\n", |
6056 | 6061 | "\n",
|
6057 |
| - "- Requires a minimum of 4 blocks before it can turn or stop\n", |
| 6062 | + "- Require a minimum of 4 blocks before it can turn or stop\n", |
6058 | 6063 | "- But can move 10 blocks before turning\n",
|
6059 | 6064 | "\n",
|
6060 |
| - "**Directing the ultra crucible from the lava pool to the machine parts factory, what is the least heat loss it can incur?**\n" |
| 6065 | + "**Directing the ultra crucible from the lava pool to the machine parts factory, what is the least heat loss it can incur?**\n", |
| 6066 | + "\n", |
| 6067 | + "**My solution:**\n", |
| 6068 | + "\n", |
| 6069 | + "- Change the `max_straight` value from 3 to 10. So I've parameterised this.\n", |
| 6070 | + "- 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", |
| 6071 | + "\n", |
| 6072 | + "```python\n", |
| 6073 | + "if straight_steps >= pre_turn\n", |
| 6074 | + "```\n", |
| 6075 | + "\n", |
| 6076 | + "- 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", |
| 6077 | + "\n", |
| 6078 | + "```python\n", |
| 6079 | + " if current_posn == goal and straight_steps >= pre_turn:\n", |
| 6080 | + " return cost\n", |
| 6081 | + "```\n", |
| 6082 | + "\n", |
| 6083 | + "And that's it!" |
6061 | 6084 | ]
|
6062 | 6085 | },
|
6063 | 6086 | {
|
|
0 commit comments