Day 10: Hoof It

2024-12-10

You all arrive at a Lava Production Facility on a floating island in the sky. As the others begin to search the massive industrial complex, you feel a small nose boop your leg and look down to discover a reindeer wearing a hard hat.

The reindeer is holding a book titled "Lava Island Hiking Guide". However, when you open the book, you discover that most of it seems to have been scorched by lava! As you're about to ask how you can help, the reindeer brings you a blank topographic map of the surrounding area (your puzzle input) and looks up at you excitedly.

Perhaps you can help fill in the missing hiking trails?

The topographic map indicates the height at each position using a scale from 0 (lowest) to 9 (highest). For example:

1
2
3
4
0123 1234 8765 9876

Based on un-scorched scraps of the book, you determine that a good hiking trail is as long as possible and has an even, gradual, uphill slope. For all practical purposes, this means that a hiking trail is any path that starts at height 0, ends at height 9, and always increases by a height of exactly 1 at each step. Hiking trails never include diagonal steps - only up, down, left, or right (from the perspective of the map).

You look up from the map and notice that the reindeer has helpfully begun to construct a small pile of pencils, markers, rulers, compasses, stickers, and other equipment you might need to update the map with hiking trails.

A trailhead is any position that starts one or more hiking trails - here, these positions will always have height 0. Assembling more fragments of pages, you establish that a trailhead's score is the number of 9-height positions reachable from that trailhead via a hiking trail. In the above example, the single trailhead in the top left corner has a score of 1 because it can reach a single 9 (the one in the bottom left).

This trailhead has a score of 2:

1
2
3
4
5
6
7
...0... ...1... ...2... 6543456 7.....7 8.....8 9.....9

(The positions marked . are impassable tiles to simplify these examples; they do not appear on your actual topographic map.)

This trailhead has a score of 4 because every 9 is reachable via a hiking trail except the one immediately to the left of the trailhead:

1
2
3
4
5
6
7
..90..9 ...1.98 ...2..7 6543456 765.987 876.... 987....

This topographic map contains two trailheads; the trailhead at the top has a score of 1, while the trailhead at the bottom has a score of 2:

1
2
3
4
5
6
7
10..9.. 2...8.. 3...7.. 4567654 ...8..3 ...9..2 .....01

Here's a larger example:

1
2
3
4
5
6
7
8
89010123 78121874 87430965 96549874 45678903 32019012 01329801 10456732

This larger example has 9 trailheads. Considering the trailheads in reading order, they have scores of 5, 6, 5, 3, 1, 3, 5, 3, and 5. Adding these scores together, the sum of the scores of all trailheads is 36.

The reindeer gleefully carries over a protractor and adds it to the pile. What is the sum of the scores of all trailheads on your topographic map?

Iterations: 0
Paths: 0

A few steps to this:

  1. Get all of the trailheads by checking for positions in the matrix that contain a 0
  2. Iterate over each trailhead
    1. Get the number of trailends for each trailhead
    2. De-duplicate the trailends for each trailhead
    3. Count the number of de-duplicated trailends for each trailhead

The code below shows how I calculated trailheads and paths. The real magic is in the getPaths method. This is a recursive method that iterates over positions around the current position, checking for the next step in the matrix. If we find the next step, recursively call the method with the next node.

What's up with type StrPosition = `${number},${number}`

In JavaScript, object equality is weird, especially if you're coming from a language like Python.

In Python, you'll typically use object_a == object_b, which works out-of-the-box for instances of Pydantic classes and dataclasses.

In JavaScript, equality checks are similar to Python's object_a is object_b.

My goal was to get an object that didn't force me to worry about JavaScript's funky object equality. By converting the coordinates to a string, instead of using an object, I could just use simple equality checks.

I could've used something like lodash's isEqual, but this was easier ¯\_(ツ)_/¯

So instead of doing deep object equality checks, I simply put the numbers into a string. That string always has the y and x coordinates within the matrix. TypeScript helps guarentee this with literal types.

Then, when I want to de-duplicate data, I can simply transform the array of strings into a set of strings.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const parser = z.array(z.string().transform((value) => value.split(""))); const values = parser.parse(input.split("\n")); interface Position { y: number; x: number; } type StrPosition = `${number},${number}`; class Map { matrix: Array<Array<string>>; constructor(matrix: Array<Array<string>>) { // Poor man's deep-copy this.matrix = JSON.parse(JSON.stringify(matrix)); } get trailheads() { const nodes: Array<Position> = []; this.matrix.forEach((row, y) => { row.forEach((character, x) => { if (character === "0") { nodes.push({ y, x }); } }); }); return nodes; } at(position: Position) { return this.matrix[position.y]?.[position.x]; } getPaths(position: Position, current: number): Array<StrPosition> { if (current === 9) { return [`${position.y},${position.x}`]; } // Check all surrounding nodes for current + 1 const paths: Array<StrPosition> = []; // Above if (this.at({ y: position.y - 1, x: position.x }) === `${current + 1}`) { paths.push(...this.getPaths({ y: position.y - 1, x: position.x }, current + 1)); } // Right if (this.at({ y: position.y, x: position.x + 1 }) === `${current + 1}`) { paths.push(...this.getPaths({ y: position.y, x: position.x + 1 }, current + 1)); } // Below if (this.at({ y: position.y + 1, x: position.x }) === `${current + 1}`) { paths.push(...this.getPaths({ y: position.y + 1, x: position.x }, current + 1)); } // Left if (this.at({ y: position.y, x: position.x - 1 }) === `${current + 1}`) { paths.push(...this.getPaths({ y: position.y, x: position.x - 1 }, current + 1)); } return paths; } }

The reindeer spends a few minutes reviewing your hiking trail map before realizing something, disappearing for a few minutes, and finally returning with yet another slightly-charred piece of paper.

The paper describes a second way to measure a trailhead called its rating. A trailhead's rating is the number of distinct hiking trails which begin at that trailhead. For example:

1
2
3
4
5
6
7
.....0. ..4321. ..5..2. ..6543. ..7..4. ..8765. ..9....

The above map has a single trailhead; its rating is 3 because there are exactly three distinct hiking trails which begin at that position:

1
2
3
4
5
6
7
.....0. .....0. .....0. ..4321. .....1. .....1. ..5.... .....2. .....2. ..6.... ..6543. .....3. ..7.... ..7.... .....4. ..8.... ..8.... ..8765. ..9.... ..9.... ..9....

Here is a map containing a single trailhead with rating 13:

1
2
3
4
5
6
7
..90..9 ...1.98 ...2..7 6543456 765.987 876.... 987....

This map contains a single trailhead with rating 227 (because there are 121 distinct hiking trails that lead to the 9 on the right edge and 106 that lead to the 9 on the bottom edge):

1
2
3
4
5
6
012345 123456 234567 345678 4.6789 56789.

Here's the larger example from before:

1
2
3
4
5
6
7
8
89010123 78121874 87430965 96549874 45678903 32019012 01329801 10456732

Considering its trailheads in reading order, they have ratings of 20, 24, 10, 4, 1, 4, 5, 8, and 5. The sum of all trailhead ratings in this larger example topographic map is 81.

You're not sure how, but the reindeer seems to have crafted some tiny flags out of toothpicks and bits of paper and is using them to mark trailheads on your topographic map. What is the sum of the ratings of all trailheads?

Iterations: 0
Paths: 0

This uses the exact same code as Part 1, but I don't de-duplicate trailends.

Part 1 TimePart 1 RankPart 2 TimePart 2 Rank
00:22:143,45900:23:562,795

This was fairly easy, especially for a Day 10 problem. I'm super proud of my Part 2 solution, which only required me to change two lines of code from my Part 1 solution. All of the time spent on Part 2 was double-checking the given examples to make sure I didn't miss anything.

Part 1

1
2
3
const trailends = new Set(map.getPaths(trailhead, 0)); runningTotal += trailends.size;

Part 2

1
2
3
const trailends = map.getPaths(trailhead, 0); runningTotal += trailends.length;