Day 10: Hoof It
by @SethTisue
Puzzle description
https://adventofcode.com/2024/day/10
Summary
Like many Advent of Code puzzles, this is a graph search problem. Such problems are highly amenable to recursive solutions.
In large graphs, it may be necessary to memoize intermediate results in order to get good performance. But here, it turns out that the graphs are small enough that recursion alone does the job just fine.
Shared code
Let's start by representing coordinate pairs:
type Pos = (Int, Int)
extension (pos: Pos)
def +(other: Pos): Pos =
(pos(0) + other(0), pos(1) + other(1))
and the input grid:
type Topo = Vector[Vector[Int]]
extension (topo: Topo)
def apply(pos: Pos): Int =
topo(pos(0))(pos(1))
def inBounds(pos: Pos): Boolean =
pos(0) >= 0 && pos(0) < topo.size &&
pos(1) >= 0 && pos(1) < topo.head.size
def positions =
for row <- topo.indices
column <- topo.head.indices
yield (row, column)
So far this is all quite typical code that is usable in many Advent of Code puzzles.
Reading the input is typical as well:
def getInput(name: String): Topo =
io.Source.fromResource(name)
.getLines
.map(_.map(_.asDigit).toVector)
.toVector
In order to avoid doing coordinate math all the time, let's turn the grid into a graph by analyzing which cells are actually connected to each other. Each cell can only have a small number of "reachable" neighbors -- those neighbors that are exactly 1 higher than us.
type Graph = Map[Pos, Set[Pos]]
def computeGraph(topo: Topo): Graph =
def reachableNeighbors(pos: Pos): Set[Pos] =
Set((-1, 0), (1, 0), (0, -1), (0, 1))
.flatMap: offsets =>
Some(pos + offsets)
.filter: nextPos =>
topo.inBounds(nextPos) && topo(nextPos) == topo(pos) + 1
topo.positions
.map(pos => pos -> reachableNeighbors(pos))
.toMap
with this graph structure in hand, we can forget about the grid and solve the problem at a higher level of abstraction.
Part 1
Part 1 is actually more difficult than part 2, in my opinion. In fact, in my first attempt to solve part 1, I accidentally solved part 2! Once I saw part 2, I had to go back and reconstruct what I had done earlier.
From a given trailhead, the same summit may be reachable by multiple
routes. Therefore, we can't just count routes; we must remember what
the destinations are. Hence, the type of the recursive method is
Set[Pos]
-- the set of summits that are reachable from the current
position.
def solve1(topo: Topo): Int =
val graph = computeGraph(topo)
def reachableSummits(pos: Pos): Set[Pos] =
if topo(pos) == 9
then Set(pos)
else graph(pos).flatMap(reachableSummits)
topo.positions
.filter(pos => topo(pos) == 0)
.map(pos => reachableSummits(pos).size)
.sum
def part1(name: String): Int =
solve1(getInput(name))
As mentioned earlier, note that we don't bother memoizing. That means we're doing some redundant computation (when paths branch and then rejoin), but the code runs plenty fast anyway on the size of input that we have.
Part 2
The code for part 2 is nearly identical. We no longer need to de-duplicate
routes that have the same destination, so it's now sufficient for the recursion
to return Int
.
It would certainly be possible to refactor this to share more code with part 1, but I've chosen to leave it this way.
def solve2(topo: Topo): Int =
val graph = computeGraph(topo)
def routes(pos: Pos): Int =
if topo(pos) == 9
then 1
else graph(pos).toSeq.map(routes).sum
topo.positions
.filter(pos => topo(pos) == 0)
.map(routes)
.sum
def part2(name: String): Int =
solve2(getInput(name))
One tricky bit here is the necessity to include toSeq
when
recursing. That's because we have a Set[Pos]
, but if we .map(...)
on a Set
, the result will also be a Set
. But we don't want to
throw away duplicate counts.
Solutions from the community
- Solution by Artem Nikiforov
- Solution by Spamegg
- Solution by Samuel Chassot
- Solution by Raphaël Marbeck
- Solution by nichobi
- Solution by Roland Tritsch
- Solution by Maciej Gorywoda
- Solution by jnclt
- Solution by Antoine Amiguet
- Solution by Joshua Portway
- Solution by scarf
- Solution by Bulby
- Solution by Philippus Baalman
Share your solution to the Scala community by editing this page. You can even write the whole article! See here for the expected format