Skip to content

Commit cd79926

Browse files
authored
Day 10 article (#691)
1 parent bb19837 commit cd79926

File tree

1 file changed

+140
-1
lines changed

1 file changed

+140
-1
lines changed

docs/2024/puzzles/day10.md

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,154 @@ import Solver from "../../../../../website/src/components/Solver.js"
22

33
# Day 10: Hoof It
44

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

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

11+
## Summary
12+
13+
Like many Advent of Code puzzles, this is a graph search problem.
14+
Such problems are highly amenable to recursive solutions.
15+
16+
In large graphs, it may be necessary to memoize intermediate results
17+
in order to get good performance. But here, it turns out that the
18+
graphs are small enough that recursion alone does the job just fine.
19+
20+
### Shared code
21+
22+
Let's start by representing coordinate pairs:
23+
24+
```scala
25+
type Pos = (Int, Int)
26+
extension (pos: Pos)
27+
def +(other: Pos): Pos =
28+
(pos(0) + other(0), pos(1) + other(1))
29+
```
30+
31+
and the input grid:
32+
33+
```scala
34+
type Topo = Vector[Vector[Int]]
35+
extension (topo: Topo)
36+
def apply(pos: Pos): Int =
37+
topo(pos(0))(pos(1))
38+
def inBounds(pos: Pos): Boolean =
39+
pos(0) >= 0 && pos(0) < topo.size &&
40+
pos(1) >= 0 && pos(1) < topo.head.size
41+
def positions =
42+
for row <- topo.indices
43+
column <- topo.head.indices
44+
yield (row, column)
45+
```
46+
47+
So far this is all quite typical code that is usable in many
48+
Advent of Code puzzles.
49+
50+
Reading the input is typical as well:
51+
52+
```scala
53+
def getInput(name: String): Topo =
54+
io.Source.fromResource(name)
55+
.getLines
56+
.map(_.map(_.asDigit).toVector)
57+
.toVector
58+
```
59+
60+
In order to avoid doing coordinate math all the time, let's turn the
61+
grid into a graph by analyzing which cells are actually connected to
62+
each other. Each cell can only have a small number of "reachable"
63+
neighbors -- those neighbors that are exactly 1 higher than us.
64+
65+
```scala
66+
type Graph = Map[Pos, Set[Pos]]
67+
68+
def computeGraph(topo: Topo): Graph =
69+
def reachableNeighbors(pos: Pos): Set[Pos] =
70+
Set((-1, 0), (1, 0), (0, -1), (0, 1))
71+
.flatMap: offsets =>
72+
Some(pos + offsets)
73+
.filter: nextPos =>
74+
topo.inBounds(nextPos) && topo(nextPos) == topo(pos) + 1
75+
topo.positions
76+
.map(pos => pos -> reachableNeighbors(pos))
77+
.toMap
78+
```
79+
80+
with this graph structure in hand, we can forget about the grid
81+
and solve the problem at a higher level of abstraction.
82+
83+
### Part 1
84+
85+
Part 1 is actually more difficult than part 2, in my opinion. In
86+
fact, in my first attempt to solve part 1, I accidentally solved part
87+
2! Once I saw part 2, I had to go back and reconstruct what I had done
88+
earlier.
89+
90+
From a given trailhead, the same summit may be reachable by multiple
91+
routes. Therefore, we can't just count routes; we must remember what
92+
the destinations are. Hence, the type of the recursive method is
93+
`Set[Pos]` -- the set of summits that are reachable from the current
94+
position.
95+
96+
```scala
97+
def solve1(topo: Topo): Int =
98+
val graph = computeGraph(topo)
99+
def reachableSummits(pos: Pos): Set[Pos] =
100+
if topo(pos) == 9
101+
then Set(pos)
102+
else graph(pos).flatMap(reachableSummits)
103+
topo.positions
104+
.filter(pos => topo(pos) == 0)
105+
.map(pos => reachableSummits(pos).size)
106+
.sum
107+
108+
def part1(name: String): Int =
109+
solve1(getInput(name))
110+
```
111+
112+
As mentioned earlier, note that we don't bother memoizing. That means
113+
we're doing some redundant computation (when paths branch and then
114+
rejoin), but the code runs plenty fast anyway on the size of input
115+
that we have.
116+
117+
### Part 2
118+
119+
The code for part 2 is nearly identical. We no longer need to de-duplicate
120+
routes that have the same destination, so it's now sufficient for the recursion
121+
to return `Int`.
122+
123+
It would certainly be possible to refactor this to share more code
124+
with part 1, but I've chosen to leave it this way.
125+
126+
```scala
127+
def solve2(topo: Topo): Int =
128+
val graph = computeGraph(topo)
129+
def routes(pos: Pos): Int =
130+
if topo(pos) == 9
131+
then 1
132+
else graph(pos).toSeq.map(routes).sum
133+
topo.positions
134+
.filter(pos => topo(pos) == 0)
135+
.map(routes)
136+
.sum
137+
138+
def part2(name: String): Int =
139+
solve2(getInput(name))
140+
```
141+
142+
One tricky bit here is the necessity to include `toSeq` when
143+
recursing. That's because we have a `Set[Pos]`, but if we `.map(...)`
144+
on a `Set`, the result will also be a `Set`. But we don't want to
145+
throw away duplicate counts.
146+
9147
## Solutions from the community
148+
10149
- [Solution](https://github.com/nikiforo/aoc24/blob/main/src/main/scala/io/github/nikiforo/aoc24/D10T2.scala) by [Artem Nikiforov](https://github.com/nikiforo)
11150
- [Solution](https://github.com/spamegg1/aoc/blob/master/2024/10/10.worksheet.sc#L166) by [Spamegg](https://github.com/spamegg1)
12151
- [Solution](https://github.com/samuelchassot/AdventCode_2024/blob/8cc89587c8558c7f55e2e0a3d6868290f0c5a739/10/Day10.scala) by [Samuel Chassot](https://github.com/samuelchassot)
13-
- [Solution](https://github.com/rmarbeck/advent2024/blob/main/day10/src/main/scala/Solution.scala) by [Raphaël Marbeck](https://github.com/rmarbeck)
152+
- [Solution](https://github.com/rmarbeck/advent2024/blob/main/day10/src/main/scala/Solution.scala) by [Raphaël Marbeck](https://github.com/rmarbeck)
14153
- [Solution](https://github.com/nichobi/advent-of-code-2024/blob/main/10/solution.scala) by [nichobi](https://github.com/nichobi)
15154
- [Solution](https://github.com/rolandtritsch/scala3-aoc-2024/blob/trunk/src/aoc2024/Day10.scala) by [Roland Tritsch](https://github.com/rolandtritsch)
16155
- [Solution](https://github.com/makingthematrix/AdventOfCode2024/blob/main/src/main/scala/io/github/makingthematrix/AdventofCode2024/DayTen.scala) by [Maciej Gorywoda](https://github.com/makingthematrix)

0 commit comments

Comments
 (0)