Skip to content

Commit 4e4c99b

Browse files
authored
docs(2024/20): add community writeup (#839)
* docs(2024/20): add community writeup * Update docs/2024/puzzles/day20.md
1 parent f959c6e commit 4e4c99b

File tree

1 file changed

+253
-0
lines changed

1 file changed

+253
-0
lines changed

docs/2024/puzzles/day20.md

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,269 @@ import Solver from "../../../../../website/src/components/Solver.js"
22

33
# Day 20: Race Condition
44

5+
by [@scarf005](https://github.com/scarf005)
6+
57
## Puzzle description
68

79
https://adventofcode.com/2024/day/20
810

11+
## Solution Summary
12+
13+
Both parts of the problem are essentially the same - they ask you to find the shortest 'cheated' path. The only difference is the duration you can cheat.
14+
15+
1. Parse the input into start point, end point, and path
16+
2. For each step, collect a list of all possible cheated positions
17+
3. Sum cheats that would save at least 100 picoseconds
18+
19+
## Data Structures
20+
21+
Let's define the core data structure first: `Pos`. It represents a `(x,y)` position in the grid. Since `Pos` will be used _extensively_, we should make it as performant as possible. It's defined as an [opaque type](https://docs.scala-lang.org/scala3/book/types-opaque-types.html), giving it the performance of `Int` while maintaining the ergonomics of a regular case object `Pos` with `x` and `y` fields.
22+
23+
```scala
24+
opaque type Pos = Int // 1)
25+
26+
object Pos:
27+
val up = Pos(0, -1)
28+
val down = Pos(0, 1)
29+
val left = Pos(-1, 0)
30+
val right = Pos(1, 0)
31+
val zero = Pos(0, 0)
32+
inline def apply(x: Int, y: Int): Pos = y << 16 | x // 2)
33+
34+
extension (p: Pos)
35+
inline def x = p & 0xffff // 3)
36+
inline def y = p >> 16
37+
inline def neighbors: List[Pos] =
38+
List(p + up, p + right, p + down, p + left)
39+
inline def +(q: Pos): Pos = Pos(p.x + q.x, p.y + q.y)
40+
inline infix def taxiDist(q: Pos) = (p.x - q.x).abs + (p.y - q.y).abs
41+
```
42+
43+
- `1)`: A new type `Pos` is declared that isn't interchangeable with `Int`.
44+
- `2)`: An `Int` is 32 bits. The lower 16 bits store `x` and the upper 16 bits store `y`. This is safe as the width and height of the given map won't exceed 65536 tiles (2^16).
45+
- `3)`: `AND`ing with `0xffff` (=`0x0000ffff`) keeps only the lower 16 bits, effectively extracting `x`.
46+
47+
:::info
48+
49+
To better illustrate how `Pos` works, here's the hexadecimal layout:
50+
51+
```
52+
// 0x 00ff 0001
53+
// hex y position x position
54+
55+
scala> 0x00ff0001
56+
val res1: Int = 16711681
57+
58+
// bit-shift to extract the y poisition 0x00ff
59+
scala> 0x00ff0001 >> 16
60+
val res2: Int = 255
61+
62+
// AND it to extract the x position 0001
63+
scala> 0x00ff0001 & 0x0000ffff
64+
val res3: Int = 1
65+
```
66+
67+
:::
68+
69+
The `Rect` case class marks the boundaries of the track.
70+
71+
```scala
72+
extension (x: Int) inline def ±(y: Int) = x - y to x + y // 1)
73+
extension (x: Inclusive)
74+
inline def &(y: Inclusive) = (x.start max y.start) to (x.end min y.end) // 2)
75+
76+
case class Rect(x: Inclusive, y: Inclusive):
77+
inline def &(that: Rect) = Rect(x & that.x, y & that.y) // 3)
78+
79+
def iterator: Iterator[Pos] = for // 4)
80+
y <- y.iterator
81+
x <- x.iterator
82+
yield Pos(x, y)
83+
```
84+
85+
- `1)`: Convenience method to create a range from `x-y` to `x+y`.
86+
- `2)`: Convenience method to create a range from the intersection of two ranges.
87+
- `3)`: Convenience method to create a new `Rect` from the intersection of two Rects.
88+
- `4)`: `O(1)` space iterator to iterate over all positions in the `Rect`.
89+
90+
## Parsing
91+
92+
The input is a large maze with a **single path** from start to end.
93+
Since the solution involves moving forward in the path frequently, storing the path as a set is more efficient.
94+
95+
```scala
96+
case class Track(start: Pos, end: Pos, walls: Set[Pos], bounds: Rect)
97+
98+
object Track:
99+
def parse(input: String) =
100+
val lines = input.trim.split('\n')
101+
val bounds = Rect(0 to lines.head.size - 1, 0 to lines.size - 1)
102+
val track = Track(Pos.zero, Pos.zero, Set.empty, bounds)
103+
bounds.iterator.foldLeft(track) { (track, p) =>
104+
lines(p.y)(p.x) match
105+
case 'S' => track.copy(start = p)
106+
case 'E' => track.copy(end = p)
107+
case '#' => track.copy(walls = track.walls + p)
108+
case _ => track
109+
}
110+
```
111+
112+
As there's only 1 path, this algorithm works well:
113+
114+
1. Get 4 directions from current position.
115+
2. Filter out walls and last position.
116+
3. Repeat until end is reached.
117+
118+
```scala
119+
case class Track(start: Pos, end: Pos, walls: Set[Pos], bounds: Rect):
120+
lazy val path: Vector[Pos] = // 1)
121+
inline def canMove(prev: List[Pos])(p: Pos) =
122+
!walls.contains(p) && Some(p) != prev.headOption // 2)
123+
124+
@tailrec def go(xs: List[Pos]): List[Pos] = xs match
125+
case Nil => Nil
126+
case p :: _ if p == end => xs
127+
case p :: ys => go(p.neighbors.filter(canMove(ys)) ++ xs)
128+
129+
go(List(start)).reverseIterator.toVector // 3)
130+
```
131+
132+
- `1)`: It needs to be `lazy val`, otherwise path will be initialized in `Track.parse`.
133+
- `2)`: `ys.headOption` gets the last position.
134+
- `3)`: We reverse it since the constructed path is from end to start.
135+
136+
## Solution
137+
138+
Now that we have the path from start to end, we can calculate how much time we can save using cheats with this algorithm:
139+
140+
```scala
141+
// ...
142+
lazy val zipped = path.zipWithIndex
143+
lazy val pathMap = zipped.toMap
144+
145+
def cheatedPaths(maxDist: Int) =
146+
def radius(p: Pos) =
147+
(Rect(p.x ± maxDist, p.y ± maxDist) & bounds).iterator
148+
.filter(p.taxiDist(_) <= maxDist)
149+
150+
zipped.map { (p, i) => // 1)
151+
radius(p) // 2)
152+
.flatMap(pathMap.get) // 3)
153+
.map { j => (j - i) - (p taxiDist path(j)) } // 4)
154+
.count(_ >= 100) // 5)
155+
}.sum
156+
```
157+
158+
1. For all points in the path:
159+
2. Get all candidate cheated destinations.
160+
3. Filter out destinations not in the path (otherwise we'll be stuck in a wall!).
161+
4. Calculate time saved by cheating by subtracting cheated time (`(p taxiDist path(j))`) from original time (`(j - i)`).
162+
5. Filter out all cheats that save at least 100 picoseconds.
163+
164+
Since parts 1 and 2 only differ in the number of picoseconds you can cheat, implementing them is trivial:
165+
166+
```scala
167+
def part1(input: String) =
168+
val track = Track.parse(input)
169+
track.cheatedPaths(2)
170+
171+
def part2(input: String) =
172+
val track = Track.parse(input)
173+
track.cheatedPaths(20)
174+
```
175+
176+
## Final Code
177+
178+
```scala
179+
import scala.annotation.tailrec
180+
import scala.collection.immutable.Range.Inclusive
181+
182+
extension (x: Int) inline def ±(y: Int) = x - y to x + y
183+
extension (x: Inclusive)
184+
inline def &(y: Inclusive) = (x.start max y.start) to (x.end min y.end)
185+
186+
opaque type Pos = Int
187+
188+
object Pos:
189+
val up = Pos(0, -1)
190+
val down = Pos(0, 1)
191+
val left = Pos(-1, 0)
192+
val right = Pos(1, 0)
193+
val zero = Pos(0, 0)
194+
inline def apply(x: Int, y: Int): Pos = y << 16 | x
195+
196+
extension (p: Pos)
197+
inline def x = p & 0xffff
198+
inline def y = p >> 16
199+
inline def neighbors: List[Pos] =
200+
List(p + up, p + right, p + down, p + left)
201+
inline def +(q: Pos): Pos = Pos(p.x + q.x, p.y + q.y)
202+
inline infix def taxiDist(q: Pos) = (p.x - q.x).abs + (p.y - q.y).abs
203+
204+
case class Rect(x: Inclusive, y: Inclusive):
205+
inline def &(that: Rect) = Rect(x & that.x, y & that.y)
206+
207+
def iterator: Iterator[Pos] = for
208+
y <- y.iterator
209+
x <- x.iterator
210+
yield Pos(x, y)
211+
212+
object Track:
213+
def parse(input: String) =
214+
val lines = input.trim.split('\n')
215+
val bounds = Rect(0 to lines.head.size - 1, 0 to lines.size - 1)
216+
val track = Track(Pos.zero, Pos.zero, Set.empty, bounds)
217+
bounds.iterator.foldLeft(track) { (track, p) =>
218+
lines(p.y)(p.x) match
219+
case 'S' => track.copy(start = p)
220+
case 'E' => track.copy(end = p)
221+
case '#' => track.copy(walls = track.walls + p)
222+
case _ => track
223+
}
224+
225+
case class Track(start: Pos, end: Pos, walls: Set[Pos], bounds: Rect):
226+
lazy val path: Vector[Pos] =
227+
inline def canMove(prev: List[Pos])(p: Pos) =
228+
!walls.contains(p) && Some(p) != prev.headOption
229+
230+
@tailrec def go(xs: List[Pos]): List[Pos] = xs match
231+
case Nil => Nil
232+
case p :: _ if p == end => xs
233+
case p :: ys => go(p.neighbors.filter(canMove(ys)) ++ xs)
234+
235+
go(List(start)).reverseIterator.toVector
236+
237+
lazy val zipped = path.zipWithIndex
238+
lazy val pathMap = zipped.toMap
239+
240+
def cheatedPaths(maxDist: Int) =
241+
def radius(p: Pos) =
242+
(Rect(p.x ± maxDist, p.y ± maxDist) & bounds).iterator
243+
.filter(p.taxiDist(_) <= maxDist)
244+
245+
zipped.map { (p, i) =>
246+
radius(p)
247+
.flatMap(pathMap.get)
248+
.map { j => (j - i) - (p taxiDist path(j)) }
249+
.count(_ >= 100)
250+
}.sum
251+
252+
def part1(input: String) =
253+
val track = Track.parse(input)
254+
track.cheatedPaths(2)
255+
256+
def part2(input: String) =
257+
val track = Track.parse(input)
258+
track.cheatedPaths(20)
259+
```
260+
9261
## Solutions from the community
10262

11263
- [Solution](https://github.com/rmarbeck/advent2024/blob/main/day20/src/main/scala/Solution.scala) by [Raphaël Marbeck](https://github.com/rmarbeck)
12264
- [Solution](https://github.com/nikiforo/aoc24/blob/main/src/main/scala/io/github/nikiforo/aoc24/D20T2.scala) by [Artem Nikiforov](https://github.com/nikiforo)
13265
- [Solution](https://github.com/aamiguet/advent-2024/blob/main/src/main/scala/ch/aamiguet/advent2024/Day20.scala) by [Antoine Amiguet](https://github.com/aamiguet)
14266
- [Solution](https://github.com/merlinorg/aoc2024/blob/main/src/main/scala/Day20.scala) by [merlinorg](https://github.com/merlinorg)
267+
- [Solution](https://github.com/scarf005/aoc-scala/blob/main/2024/day20.scala) by [scarf](https://github.com/scarf005)
15268
- [Writeup](https://thedrawingcoder-gamer.github.io/aoc-writeups/2024/day20.html) by [Bulby](https://github.com/TheDrawingCoder-Gamer)
16269
- [Solution](https://github.com/AvaPL/Advent-of-Code-2024/tree/main/src/main/scala/day20) by [Paweł Cembaluk](https://github.com/AvaPL)
17270

0 commit comments

Comments
 (0)