You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/2023/puzzles/day23.md
+65-34Lines changed: 65 additions & 34 deletions
Original file line number
Diff line number
Diff line change
@@ -1,5 +1,6 @@
1
1
import Solver from "../../../../../website/src/components/Solver.js"
2
2
import Literate from "../../../../../website/src/components/Literate.js"
3
+
import ExpandImage from "../../../../../website/src/components/ExpandImage.js"
3
4
4
5
# Day 23: A Long Walk
5
6
@@ -25,7 +26,13 @@ Each junction might have two or three adjacent paths we can enter. When we exit
25
26
26
27
- For each iteration of the search through this graph, we check all adjacent junctions against the visited set. When using a hash set, this will result in computing hashes of position coordinates tens of millions of times. We can avoid this by giving each junction an index and using a BitSet of these indices as our visited set. Checking for membership in a BitSet only requires a bitshift and a bitwise AND mask. On my machine, this drops the run time from ~7 seconds to ~2 seconds (70%).
27
28
28
-
For part 1, neither of these optimizations are necessary. To understand why, notice that every junction is surrounded by slopes. When a junction is surrounded by four slopes, as most of them are, two are incoming and two are outgoing. For part 1, these are arranged in such a way that the adjacency graph becomes a directed acyclic graph, with a greatly reduced search space. One way to notice this early on is to generate a [visualization via GraphViz](https://dreampuf.github.io/GraphvizOnline/#digraph%20G%20%7B%0A%220%22%20-%3E%20%221%22%20%5Blabel%3D159%5D%3B%0A%221%22%20-%3E%20%223%22%20%5Blabel%3D138%5D%3B%0A%221%22%20-%3E%20%222%22%20%5Blabel%3D80%5D%3B%0A%222%22%20-%3E%20%224%22%20%5Blabel%3D170%5D%3B%0A%222%22%20-%3E%20%225%22%20%5Blabel%3D310%5D%3B%0A%223%22%20-%3E%20%224%22%20%5Blabel%3D72%5D%3B%0A%223%22%20-%3E%20%226%22%20%5Blabel%3D334%5D%3B%0A%224%22%20-%3E%20%2211%22%20%5Blabel%3D232%5D%3B%0A%224%22%20-%3E%20%229%22%20%5Blabel%3D116%5D%3B%0A%225%22%20-%3E%20%2211%22%20%5Blabel%3D184%5D%3B%0A%225%22%20-%3E%20%227%22%20%5Blabel%3D112%5D%3B%0A%226%22%20-%3E%20%229%22%20%5Blabel%3D38%5D%3B%0A%226%22%20-%3E%20%228%22%20%5Blabel%3D44%5D%3B%0A%227%22%20-%3E%20%2212%22%20%5Blabel%3D222%5D%3B%0A%227%22%20-%3E%20%2213%22%20%5Blabel%3D66%5D%3B%0A%228%22%20-%3E%20%2210%22%20%5Blabel%3D38%5D%3B%0A%228%22%20-%3E%20%2215%22%20%5Blabel%3D440%5D%3B%0A%229%22%20-%3E%20%2214%22%20%5Blabel%3D188%5D%3B%0A%229%22%20-%3E%20%2210%22%20%5Blabel%3D20%5D%3B%0A%2210%22%20-%3E%20%2216%22%20%5Blabel%3D120%5D%3B%0A%2210%22%20-%3E%20%2219%22%20%5Blabel%3D222%5D%3B%0A%2211%22%20-%3E%20%2214%22%20%5Blabel%3D72%5D%3B%0A%2211%22%20-%3E%20%2213%22%20%5Blabel%3D74%5D%3B%0A%2212%22%20-%3E%20%2218%22%20%5Blabel%3D144%5D%3B%0A%2212%22%20-%3E%20%2220%22%20%5Blabel%3D520%5D%3B%0A%2213%22%20-%3E%20%2217%22%20%5Blabel%3D202%5D%3B%0A%2213%22%20-%3E%20%2218%22%20%5Blabel%3D140%5D%3B%0A%2214%22%20-%3E%20%2216%22%20%5Blabel%3D196%5D%3B%0A%2214%22%20-%3E%20%2217%22%20%5Blabel%3D152%5D%3B%0A%2215%22%20-%3E%20%2219%22%20%5Blabel%3D184%5D%3B%0A%2215%22%20-%3E%20%2222%22%20%5Blabel%3D472%5D%3B%0A%2216%22%20-%3E%20%2223%22%20%5Blabel%3D94%5D%3B%0A%2216%22%20-%3E%20%2221%22%20%5Blabel%3D198%5D%3B%0A%2217%22%20-%3E%20%2224%22%20%5Blabel%3D56%5D%3B%0A%2217%22%20-%3E%20%2221%22%20%5Blabel%3D102%5D%3B%0A%2218%22%20-%3E%20%2224%22%20%5Blabel%3D146%5D%3B%0A%2218%22%20-%3E%20%2220%22%20%5Blabel%3D60%5D%3B%0A%2219%22%20-%3E%20%2222%22%20%5Blabel%3D60%5D%3B%0A%2219%22%20-%3E%20%2223%22%20%5Blabel%3D40%5D%3B%0A%2220%22%20-%3E%20%2228%22%20%5Blabel%3D280%5D%3B%0A%2221%22%20-%3E%20%2227%22%20%5Blabel%3D162%5D%3B%0A%2221%22%20-%3E%20%2226%22%20%5Blabel%3D82%5D%3B%0A%2222%22%20-%3E%20%2225%22%20%5Blabel%3D124%5D%3B%0A%2223%22%20-%3E%20%2226%22%20%5Blabel%3D190%5D%3B%0A%2223%22%20-%3E%20%2225%22%20%5Blabel%3D112%5D%3B%0A%2224%22%20-%3E%20%2227%22%20%5Blabel%3D104%5D%3B%0A%2224%22%20-%3E%20%2228%22%20%5Blabel%3D250%5D%3B%0A%2225%22%20-%3E%20%2230%22%20%5Blabel%3D336%5D%3B%0A%2226%22%20-%3E%20%2231%22%20%5Blabel%3D128%5D%3B%0A%2226%22%20-%3E%20%2230%22%20%5Blabel%3D98%5D%3B%0A%2227%22%20-%3E%20%2229%22%20%5Blabel%3D156%5D%3B%0A%2227%22%20-%3E%20%2231%22%20%5Blabel%3D132%5D%3B%0A%2228%22%20-%3E%20%2229%22%20%5Blabel%3D58%5D%3B%0A%2229%22%20-%3E%20%2232%22%20%5Blabel%3D392%5D%3B%0A%2230%22%20-%3E%20%2233%22%20%5Blabel%3D188%5D%3B%0A%2231%22%20-%3E%20%2233%22%20%5Blabel%3D62%5D%3B%0A%2231%22%20-%3E%20%2232%22%20%5Blabel%3D84%5D%3B%0A%2232%22%20-%3E%20%2234%22%20%5Blabel%3D92%5D%3B%0A%2233%22%20-%3E%20%2234%22%20%5Blabel%3D158%5D%3B%0A%2234%22%20-%3E%20%2235%22%20%5Blabel%3D49%5D%3B%0A%7D).
29
+
For part 1, neither of these optimizations are necessary. To understand why, notice that every junction is surrounded by slopes. When a junction is surrounded by four slopes, as most of them are, two are incoming and two are outgoing. For part 1, these are arranged in such a way that the adjacency graph becomes a directed acyclic graph, with a greatly reduced search space. One way to notice this early on is to generate a visualization via GraphViz, such as the following:
30
+
31
+
import GraphVizSvg from '/img/2023-day23/graphviz.svg';
32
+
33
+
<ExpandImage>
34
+
<GraphVizSvg />
35
+
</ExpandImage>
29
36
30
37
### Framework
31
38
@@ -88,13 +95,17 @@ So far we just have helper methods. The next few definitions are the things we'l
88
95
valjunctions:Set[Point] = walkable.filter: p =>
89
96
Dir.values.map(p.move).count(walkable) >2
90
97
.toSet + start + end
98
+
```
91
99
92
-
valslopes=Map.from[Point, Dir]:
100
+
Here we can populate which points are slopes by looking up a point with `this.apply(p)`, shortened to `this(p)`.
101
+
```scala
102
+
valslopes:Map[Point, Dir] =Map.from:
93
103
points.collect:
94
-
case p if apply(p) =='^'=> p ->Dir.N
95
-
case p if apply(p) =='v'=> p ->Dir.S
96
-
case p if apply(p) =='>'=> p ->Dir.E
97
-
case p if apply(p) =='<'=> p ->Dir.W
104
+
case p ifthis(p) =='^'=> p ->Dir.N
105
+
case p ifthis(p) =='v'=> p ->Dir.S
106
+
case p ifthis(p) =='>'=> p ->Dir.E
107
+
case p ifthis(p) =='<'=> p ->Dir.W
108
+
endMaze
98
109
```
99
110
100
111
</Literate>
@@ -112,15 +123,19 @@ Next, we need an algorithm for finding junctions that are connected to a given j
This `walk` helper method attempts to move in a given direction from a given position, accounting for walls and slopes in the maze. This alternatively could have been defined as a method on `Point` itself.
This `walk` helper method attempts to move in a given direction from a given position, accounting for walls and slopes in the maze. This alternatively could have been defined as a method on `Point` itself.
138
+
This `search` helper method walks down a path from a junction while tracking the current direction and distance. `adjacentSearch`attempts to walk recursively in directions that don't go backwards. A LazyList is used here to prevent stack overflows. If there is only one adjacent path to walk too, we continue searching that path recursively until we reach a junction, otherwise, we have reached a dead end; `None` represents the fact that no new junctions are reachable down this path.
124
139
125
140
```scala
126
141
defsearch(pos: Point, facing: Dir, dist: Int):Option[(Point, Int)] =
@@ -133,33 +148,40 @@ This `walk` helper method attempts to move in a given direction from a given pos
133
148
if adjacentSearch.size ==1then adjacentSearch.head elseNone
134
149
```
135
150
136
-
This `search` helper method walks down a path from a junction while tracking the current direction and distance. `adjacentSearch` attempts to walk recursively in directions that don't go backwards. A LazyList is used here to prevent stack overflows. If there is only one adjacent path to walk too, we continue searching that path recursively until we reach a junction, otherwise, we have reached a dead end; `None` represents the fact that no new junctions are reachable down this path.
151
+
Finally, we begin the search in each direction from our current junction, returning all the connected junctions found.
137
152
138
153
```scala
139
154
for
140
155
d <-Dir.values
141
156
p <- walk(pos, d)
142
157
junction <- search(p, d, 1)
143
158
yield junction
144
-
159
+
endconnectedJunctions
145
160
```
146
161
147
-
Finally, we begin the search in each direction from our current junction, returning all the connected junctions found.
148
-
149
162
</Literate>
150
163
151
164
### Part 1
152
165
153
166
`connectedJunctions` is sufficient to solve Part 1 quickly:
defsearch(pos: Point, dist: Int)(usingmaze: Maze):Int=
178
+
defsearch(pos: Point, dist: Int):Int=
158
179
if pos == maze.end then dist else
159
180
connectedJunctions(pos).foldLeft(0):
160
181
case (max, (n, d)) => max.max(search(n, dist + d))
161
182
162
183
search(maze.start, 0)
184
+
endlongestDownhillHike
163
185
```
164
186
165
187
This uses a recursive helper method named `search`. Beginning with `start`, we recursively search for the longest path starting at each of the connected junctions.
@@ -170,17 +192,23 @@ For part 2, we'll implement the optimization mentioned in the overview, namely,
170
192
171
193
<Literate>
172
194
173
-
We begin by assigning indices to each of the junctions, by sorting them (in any way, as long as the ordering is well-defined) and zipping with an index:
174
-
175
195
```scala
196
+
defpart2(input: String):Int=
197
+
givenMaze=Maze(parseInput(input))
198
+
longestHike
199
+
176
200
deflongestHike(usingmaze: Maze):Int=
177
201
typeIndex=Int
202
+
```
203
+
204
+
We begin by assigning indices to each of the junctions, by sorting them (in any way, as long as the ordering is well-defined) and zipping with an index:
Next, we define an adjacency graph. Since `connectedJunctinos` takes slopes into account, and we no longer care about slopes for part 2, we add both the forward and reverse directinos into our Map. Note how we translate the Point locations used by `connectedJunctions` into indices using `indexOf`, defined above:
211
+
Next, we define an adjacency graph. Since `connectedJunctions` takes slopes into account, and we no longer care about slopes for part 2, we add both the forward and reverse directions into our Map. Note how we translate the Point locations used by `connectedJunctions` into indices using `indexOf`, defined above:
184
212
185
213
```scala
186
214
valadjacent:Map[Index, List[(Index, Int)]] =
@@ -193,7 +221,7 @@ Next, we define an adjacency graph. Since `connectedJunctinos` takes slopes into
193
221
```
194
222
195
223
Finally, we perform a depth-first search that is very similar to what we used in Part 1.
196
-
The main differences are that we now use indices of junctions rather than `Point`s reperesenting current position, and we now check adjacent junctions against a BitSet of visited points, which we now track as we search recursively.
224
+
The main differences are that we now use indices of junctions rather than `Point`s representing current position, and we now check adjacent junctions against a BitSet of visited points, which we now track as we search recursively.
0 commit comments