Skip to content

Commit 1f6984a

Browse files
committed
2023d17 documented
1 parent 7fcebfe commit 1f6984a

File tree

1 file changed

+78
-55
lines changed

1 file changed

+78
-55
lines changed

src/AoC_2023/Dazbo's_Advent_of_Code_2023.ipynb

+78-55
Original file line numberDiff line numberDiff line change
@@ -680,13 +680,9 @@
680680
" return\n",
681681
" \n",
682682
" # 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",
689683
" \n",
684+
" # plot stuff\n",
685+
" \n",
690686
" # # save the plot as a frame; store the frame in-memory, using a BytesIO buffer\n",
691687
" # frame = BytesIO()\n",
692688
" # plt.savefig(frame, format='png') # save to memory, rather than file\n",
@@ -5658,54 +5654,40 @@
56585654
" return\n",
56595655
" \n",
56605656
" 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",
56705659
" \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",
56755673
" \n",
5676-
" # todo: just pull out the infra from the array, rather than the path\n",
56775674
" for point, dirn in self.path_taken:\n",
56785675
" 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",
56895677
" for dir_set, arrow in zip(dir_sets, (\"^\", \">\", \"v\", \"<\")):\n",
56905678
" if LightGrid.VECTORS_TO_ARROWS[dirn] == arrow:\n",
56915679
" dir_set.add(point)\n",
56925680
" 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",
57055687
" \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",
57095691
" \n",
57105692
" for dir_set, arrow in zip(dir_sets, (\"^\", \">\", \"v\", \"<\")):\n",
57115693
" if dir_set:\n",
@@ -5743,7 +5725,7 @@
57435725
},
57445726
{
57455727
"cell_type": "code",
5746-
"execution_count": null,
5728+
"execution_count": 165,
57475729
"metadata": {},
57485730
"outputs": [],
57495731
"source": [
@@ -5925,7 +5907,28 @@
59255907
"\n",
59265908
"**My solution:**\n",
59275909
"\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"
59295932
]
59305933
},
59315934
{
@@ -5953,6 +5956,7 @@
59535956
" queue = [] # priority queue to store current state\n",
59545957
" \n",
59555958
" 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",
59565960
" current_posn:Point = start\n",
59575961
" dirn:Optional[Point] = None # Current direction. (None when we start.)\n",
59585962
" straight_steps:int = 0 # number of steps taken in one direction without turning\n",
@@ -5989,11 +5993,12 @@
59895993
" \n",
59905994
" for neighbour, dirn, new_steps in next_states:\n",
59915995
" 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",
59976002
" \n",
59986003
" raise ValueError(\"No solution found\")"
59996004
]
@@ -6052,12 +6057,30 @@
60526057
"source": [
60536058
"### Day 17 Part 2\n",
60546059
"\n",
6055-
"We're upgrading to _ultra crucibles_:\n",
6060+
"We're upgrading to _ultra crucibles_. These:\n",
60566061
"\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",
60586063
"- But can move 10 blocks before turning\n",
60596064
"\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!"
60616084
]
60626085
},
60636086
{

0 commit comments

Comments
 (0)