Skip to content

Commit da00bd7

Browse files
committed
refactor to use given Grid
1 parent fb601dd commit da00bd7

File tree

1 file changed

+122
-69
lines changed

1 file changed

+122
-69
lines changed

docs/2023/puzzles/day17.md

Lines changed: 122 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Solver from "../../../../../website/src/components/Solver.js"
2+
import Literate from "../../../../../website/src/components/Literate.js"
23

34
# Day 17: Clumsy Crucible
45

@@ -18,7 +19,33 @@ Since the restrictions on state transformations differ in part 1 and part 2, we'
1819

1920
### Framework
2021

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+
case class Grid(grid: Vector[Vector[Int]]):
26+
val xRange = grid.head.indices
27+
val yRange = 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+
def loadGrid(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+
def grid(using Grid) = summon[Grid].grid
44+
def xRange(using Grid) = summon[Grid].xRange
45+
def yRange(using Grid) = summon[Grid].yRange
46+
```
47+
48+
Second, for convenience, let's introduce a class for presenting direction:
2249

2350
```scala
2451
enum Dir:
@@ -35,9 +62,19 @@ enum Dir:
3562
case Dir.W => S
3663
case Dir.S => E
3764
case Dir.E => N
65+
```
66+
67+
Since moving forward, turning left, and turning right are common operations, convenience methods for each are included here.
3868

69+
Third, a class for position:
70+
71+
```scala
3972
case class Point(x: Int, y: Int):
40-
def inBounds = xRange.contains(x) && yRange.contains(y)
73+
def inBounds(using Grid) =
74+
xRange.contains(x) && yRange.contains(y)
75+
76+
def heatLoss(using Grid) =
77+
if inBounds then grid(y)(x) else 0
4178

4279
def move(dir: Dir) = dir match
4380
case Dir.N => copy(y = y - 1)
@@ -46,33 +83,14 @@ case class Point(x: Int, y: Int):
4683
case Dir.W => copy(x = x - 1)
4784
```
4885

49-
Since moving forward, turning left, and turning right are common operations, convenience methods for each are included here.
50-
51-
Next, let's work with our input. We'll parse it as a 2D vector of integers:
52-
53-
```scala
54-
val grid: Vector[Vector[Int]] = Vector.from:
55-
val file = loadFileSync(s"$currentDir/../input/day17")
56-
for line <- file.split("\n")
57-
yield line.map(_.asDigit).toVector
58-
```
59-
60-
And now a few convenience methods that need the input:
61-
62-
```scala
63-
val xRange = grid.head.indices
64-
val yRange = grid.indices
65-
66-
def inBounds(p: Point) =
67-
xRange.contains(p.x) && yRange.contains(p.y)
68-
69-
def heatLoss(p: Point) =
70-
if inBounds(p) then grid(p.y)(p.x) else 0
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.
7288

7389
### Search State
7490

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>
7694

7795
```scala
7896
case class State(pos: Point, dir: Dir, streak: Int):
@@ -81,7 +99,6 @@ case class State(pos: Point, dir: Dir, streak: Int):
8199
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:
82100

83101
```scala
84-
// inside case class State:
85102
def straight: State =
86103
State(pos.move(dir), dir, streak + 1)
87104

@@ -94,6 +111,8 @@ Next let's define some methods for transitioning to new states. We know that we
94111
State(pos.move(newDir), newDir, 1)
95112
```
96113

114+
</Literate>
115+
97116
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.
98117

99118
### Dijkstra's Algorithm
@@ -102,85 +121,115 @@ Finally, let's lay the groundwork for an implementation of Dijkstra's algorithm.
102121

103122
Since our valid state transformations vary between part 1 and part 2, let's parameterize our search method by a function:
104123

124+
<Literate>
125+
105126
```scala
106-
def search(next: State => List[State]): Int
127+
import collection.mutable.{PriorityQueue, Map}
128+
129+
type StateTransform = Grid ?=> State => List[State]
130+
131+
def search(next: StateTransform)(using Grid): Int =
107132
```
108133

109134
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:
110135

111136
```scala
112-
// inside def search:
113-
import collection.mutable.{PriorityQueue, Map}
114-
115137
val minHeatLoss = Map.empty[State, Int]
116138

117139
given Ordering[State] = Ordering.by(minHeatLoss)
118140
val pq = PriorityQueue.empty[State].reverse
119141

120142
var visiting = State(Point(0, 0), Dir.E, 0)
121-
val minHeatLoss(visiting) = 0
143+
minHeatLoss(visiting) = 0
122144
```
123145

124146
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.
125147
So any state we've already visited will be discarded. This is what our loop will look like:
126148

127149
```scala
128-
// inside def search:
129150
val end = Point(xRange.max, yRange.max)
130151
while visiting.pos != end do
131152
val states = next(visiting).filterNot(minHeatLoss.contains)
132153
states.foreach: s =>
133-
minHeatLoss(s) = minHeatLoss(visiting) + heatLoss(s)
154+
minHeatLoss(s) = minHeatLoss(visiting) + s.pos.heatLoss
134155
pq.enqueue(s)
135156
visiting = pq.dequeue()
136157

137-
minHeatLoss(visting)
158+
minHeatLoss(visiting)
138159
```
139160

140-
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).
167+
```scala
168+
def solve(input: String, next: StateTransform): Int =
169+
given Grid = loadGrid(input)
170+
search(next)
171+
```
141172

142173
### Part 1
143174

144-
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:
145176

146177
```scala
147178
// Inside case class State:
148-
def nextStates: List[State] =
179+
def nextStates(using Grid): List[State] =
149180
List(straight, turnLeft, turnRight).filter: s =>
150-
inBounds(s.pos) && s.streak <= 3
181+
s.pos.inBounds && s.streak <= 3
151182
```
152183

153184
This will only ever filter out the forward movement, since moving to the left or right resets the streak to 1.
154185

186+
We can then call `solve` with `nextStates` from our entry point for `part1`:
187+
188+
```scala
189+
def part1(input: String): Int =
190+
solve(input, _.nextStates)
191+
```
192+
155193
### Part 2
156194

157195
Part 2 is similar, but our streak limit increases to 10.
158196
Furthermore, while the streak is less than four, only a forward movement is possible:
159197

160198
```scala
161199
// inside case class State:
162-
def nextStates2: List[State] =
200+
def nextStates2(using Grid): List[State] =
163201
if streak < 4 then List(straight)
164202
else List(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+
def part2(input: String): Int =
210+
solve(input, _.nextStates2)
166211
```
167212

168213
## Final Code
169214

170215
```scala
171-
import locations.Directory.currentDir
172-
import inputs.Input.loadFileSync
216+
import collection.mutable.{PriorityQueue, Map}
217+
218+
def part1(input: String): Int =
219+
solve(input, _.nextStates)
173220

174-
@main def part1: Unit =
175-
println(s"The solution is ${search(_.nextStates)}")
221+
def part2(input: String): Int =
222+
solve(input, _.nextStates2)
176223

177-
@main def part2: Unit =
178-
println(s"The solution is ${search(_.nextStates2)}")
224+
def loadGrid(input: String): Grid =
225+
Grid:
226+
Vector.from:
227+
for line <- input.split("\n")
228+
yield line.map(_.asDigit).toVector
179229

180-
def loadInput(): Vector[Vector[Int]] = Vector.from:
181-
val file = loadFileSync(s"$currentDir/../input/day17")
182-
for line <- file.split("\n")
183-
yield line.map(_.asDigit).toVector
230+
case class Grid(grid: Vector[Vector[Int]]):
231+
val xRange = grid.head.indices
232+
val yRange = grid.indices
184233

185234
enum Dir:
186235
case N, S, E, W
@@ -197,24 +246,23 @@ enum Dir:
197246
case Dir.S => E
198247
case Dir.E => N
199248

200-
val grid = loadInput()
201-
202-
val xRange = grid.head.indices
203-
val yRange = grid.indices
249+
def grid(using Grid) = summon[Grid].grid
250+
def xRange(using Grid) = summon[Grid].xRange
251+
def yRange(using Grid) = summon[Grid].yRange
204252

205253
case class Point(x: Int, y: Int):
254+
def inBounds(using Grid) =
255+
xRange.contains(x) && yRange.contains(y)
256+
257+
def heatLoss(using Grid) =
258+
if inBounds then grid(y)(x) else 0
259+
206260
def move(dir: Dir) = dir match
207261
case Dir.N => copy(y = y - 1)
208262
case Dir.S => copy(y = y + 1)
209263
case Dir.E => copy(x = x + 1)
210264
case Dir.W => copy(x = x - 1)
211265

212-
def inBounds(p: Point) =
213-
xRange.contains(p.x) && yRange.contains(p.y)
214-
215-
def heatLoss(p: Point) =
216-
if inBounds(p) then grid(p.y)(p.x) else 0
217-
218266
case class State(pos: Point, dir: Dir, streak: Int):
219267
def straight: State =
220268
State(pos.move(dir), dir, streak + 1)
@@ -227,17 +275,22 @@ case class State(pos: Point, dir: Dir, streak: Int):
227275
val newDir = dir.turnRight
228276
State(pos.move(newDir), newDir, 1)
229277

230-
def nextStates: List[State] =
278+
def nextStates(using Grid): List[State] =
231279
List(straight, turnLeft, turnRight).filter: s =>
232-
inBounds(s.pos) && s.streak <= 3
280+
s.pos.inBounds && s.streak <= 3
233281

234-
def nextStates2: List[State] =
282+
def nextStates2(using Grid): List[State] =
235283
if streak < 4 then List(straight)
236284
else List(straight, turnLeft, turnRight).filter: s =>
237-
inBounds(s.pos) && s.streak <= 10
285+
s.pos.inBounds && s.streak <= 10
286+
287+
type StateTransform = Grid ?=> State => List[State]
288+
289+
def solve(input: String, next: StateTransform): Int =
290+
given Grid = loadGrid(input)
291+
search(next)
238292

239-
def search(next: State => List[State]): Int =
240-
import collection.mutable.{PriorityQueue, Map}
293+
def search(next: StateTransform)(using Grid): Int =
241294

242295
val minHeatLoss = Map.empty[State, Int]
243296

@@ -251,11 +304,11 @@ def search(next: State => List[State]): Int =
251304
while visiting.pos != end do
252305
val states = next(visiting).filterNot(minHeatLoss.contains)
253306
states.foreach: s =>
254-
minHeatLoss(s) = minHeatLoss(visiting) + heatLoss(s.pos)
307+
minHeatLoss(s) = minHeatLoss(visiting) + s.pos.heatLoss
255308
pq.enqueue(s)
256309
visiting = pq.dequeue()
257310

258-
minHeatLoss(end)
311+
minHeatLoss(visiting)
259312
```
260313

261314
### Run it in the browser

0 commit comments

Comments
 (0)