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
import Solver from "../../../../../website/src/components/Solver.js"
2
+
import Literate from "../../../../../website/src/components/Literate.js"
2
3
3
4
# Day 17: Clumsy Crucible
4
5
@@ -18,7 +19,33 @@ Since the restrictions on state transformations differ in part 1 and part 2, we'
18
19
19
20
### Framework
20
21
21
-
First, for convenience, let's introduce classes for presenting position and direction:
22
+
First, we will need a `Grid` class to represent the possible positions, and store the heat at each position.
23
+
It will be represented by a 2D vector:
24
+
```scala
25
+
caseclassGrid(grid: Vector[Vector[Int]]):
26
+
valxRange= grid.head.indices
27
+
valyRange= grid.indices
28
+
```
29
+
30
+
We can parse the input and store it in the `Grid` class. Each line is treated as a row, and each character in the row is treated as a single column, and required to be a digit:
31
+
32
+
```scala
33
+
defloadGrid(input: String):Grid=
34
+
Grid:
35
+
Vector.from:
36
+
for line <- input.split("\n")
37
+
yield line.map(_.asDigit).toVector
38
+
```
39
+
40
+
We can define some accessors to make it more convenient to work with a `Grid` that is available in the [context](https://docs.scala-lang.org/scala3/book/ca-context-parameters.html).
41
+
42
+
```scala
43
+
defgrid(usingGrid) = summon[Grid].grid
44
+
defxRange(usingGrid) = summon[Grid].xRange
45
+
defyRange(usingGrid) = summon[Grid].yRange
46
+
```
47
+
48
+
Second, for convenience, let's introduce a class for presenting direction:
22
49
23
50
```scala
24
51
enumDir:
@@ -35,9 +62,19 @@ enum Dir:
35
62
caseDir.W=>S
36
63
caseDir.S=>E
37
64
caseDir.E=>N
65
+
```
66
+
67
+
Since moving forward, turning left, and turning right are common operations, convenience methods for each are included here.
And now a few convenience methods that need the input:
61
-
62
-
```scala
63
-
valxRange= grid.head.indices
64
-
valyRange= grid.indices
65
-
66
-
definBounds(p: Point) =
67
-
xRange.contains(p.x) && yRange.contains(p.y)
68
-
69
-
defheatLoss(p: Point) =
70
-
if inBounds(p) then grid(p.y)(p.x) else0
71
-
```
86
+
Here we provide some convenience methods for checking if a point is `inBounds` on the grid,
87
+
and the `heatLoss` of a point on the grid.
72
88
73
89
### Search State
74
90
75
-
Now we want to be able to model our state as we're searching. The state will track our position. To know what transitions are possible, we need to keep track of our streak of movements in a given direction. We'll also keep track of the heat lost while getting to a state.
91
+
Now we want to be able to model our state as we're searching. The state will track our position (`pos`). To know what transitions are possible, we need to keep track of our `streak` of movements in a given direction (`dir`). Later, we'll also keep track of the heat lost while getting to a state.
92
+
93
+
<Literate>
76
94
77
95
```scala
78
96
caseclassState(pos: Point, dir: Dir, streak: Int):
@@ -81,7 +99,6 @@ case class State(pos: Point, dir: Dir, streak: Int):
81
99
Next let's define some methods for transitioning to new states. We know that we can chose to move forward, turn left, or turn right. For now, we won't consider the restrictions from Part 1 or Part 2 on whether or not you can move forward:
82
100
83
101
```scala
84
-
// inside case class State:
85
102
defstraight:State=
86
103
State(pos.move(dir), dir, streak +1)
87
104
@@ -94,6 +111,8 @@ Next let's define some methods for transitioning to new states. We know that we
94
111
State(pos.move(newDir), newDir, 1)
95
112
```
96
113
114
+
</Literate>
115
+
97
116
Note that the streak resets to one when we turn right or turn left, since we also move the position forward in that new direction.
98
117
99
118
### Dijkstra's Algorithm
@@ -102,85 +121,115 @@ Finally, let's lay the groundwork for an implementation of Dijkstra's algorithm.
102
121
103
122
Since our valid state transformations vary between part 1 and part 2, let's parameterize our search method by a function:
104
123
124
+
<Literate>
125
+
105
126
```scala
106
-
defsearch(next: State=>List[State]):Int
127
+
importcollection.mutable.{PriorityQueue, Map}
128
+
129
+
typeStateTransform=Grid?=>State=>List[State]
130
+
131
+
defsearch(next: StateTransform)(usingGrid):Int=
107
132
```
108
133
109
134
The algorithm uses Map to track the minimum total heat loss for each state, and a Priority Queue prioritizing by this heatloss to choose the next state to visit:
110
135
111
136
```scala
112
-
// inside def search:
113
-
importcollection.mutable.{PriorityQueue, Map}
114
-
115
137
valminHeatLoss=Map.empty[State, Int]
116
138
117
139
givenOrdering[State] =Ordering.by(minHeatLoss)
118
140
valpq=PriorityQueue.empty[State].reverse
119
141
120
142
varvisiting=State(Point(0, 0), Dir.E, 0)
121
-
valminHeatLoss(visiting) =0
143
+
minHeatLoss(visiting) =0
122
144
```
123
145
124
146
As we generate new states to add to the priority Queue, we need to make sure not to add suboptimal states. The first time we visit any state, it will be with a minimum possible cost, because we're visiting this new state from an adjacent minimum heatloss state in our priority queue.
125
147
So any state we've already visited will be discarded. This is what our loop will look like:
Notice how `minHeatLoss`` is always updated to the minimum of the state we're visiting from plus the incremental heatloss of the new state we're adding to the queue.
161
+
</Literate>
162
+
163
+
Notice how `minHeatLoss` is always updated to the minimum of the state we're visiting from plus the incremental heatloss of the new state we're adding to the queue.
164
+
165
+
We can then provide a framework for calling the `search` function using the input with `solve`.
166
+
It parses the input to a `Grid`, defining it as a [given instance](https://docs.scala-lang.org/scala3/book/ca-context-parameters.html).
Now we need to model our state transformation restrictions for Part 1. We can typically move straight, left, and right, but we need to make sure our streak straight streak never exceeds 3:
175
+
Now we need to model our state transformation restrictions for Part 1. We can typically move straight, left, and right, but we need to make sure our streak while moving straight never exceeds 3:
145
176
146
177
```scala
147
178
// Inside case class State:
148
-
defnextStates:List[State] =
179
+
defnextStates(usingGrid):List[State] =
149
180
List(straight, turnLeft, turnRight).filter: s =>
150
-
inBounds(s.pos)&& s.streak <=3
181
+
s.pos.inBounds&& s.streak <=3
151
182
```
152
183
153
184
This will only ever filter out the forward movement, since moving to the left or right resets the streak to 1.
154
185
186
+
We can then call `solve` with `nextStates` from our entry point for `part1`:
187
+
188
+
```scala
189
+
defpart1(input: String):Int=
190
+
solve(input, _.nextStates)
191
+
```
192
+
155
193
### Part 2
156
194
157
195
Part 2 is similar, but our streak limit increases to 10.
158
196
Furthermore, while the streak is less than four, only a forward movement is possible:
159
197
160
198
```scala
161
199
// inside case class State:
162
-
defnextStates2:List[State] =
200
+
defnextStates2(usingGrid):List[State] =
163
201
if streak <4thenList(straight)
164
202
elseList(straight, turnLeft, turnRight).filter: s =>
165
-
inBounds(s.pos) && s.streak <=10
203
+
s.pos.inBounds && s.streak <=10
204
+
```
205
+
206
+
And we call solve with `nextStates2` to solve `part2`:
207
+
208
+
```scala
209
+
defpart2(input: String):Int=
210
+
solve(input, _.nextStates2)
166
211
```
167
212
168
213
## Final Code
169
214
170
215
```scala
171
-
importlocations.Directory.currentDir
172
-
importinputs.Input.loadFileSync
216
+
importcollection.mutable.{PriorityQueue, Map}
217
+
218
+
defpart1(input: String):Int=
219
+
solve(input, _.nextStates)
173
220
174
-
@main defpart1:Unit=
175
-
println(s"The solution is ${search(_.nextStates)}")
221
+
defpart2(input: String):Int=
222
+
solve(input, _.nextStates2)
176
223
177
-
@main defpart2:Unit=
178
-
println(s"The solution is ${search(_.nextStates2)}")
0 commit comments