Skip to content

Day 10. Hoof It

We have arrived at the famous lava production facility on Lava Island, and the reindeer need our help

Our task is to find hike paths from 0 to 9 (0->1->2...->9) on the grid.

Grid time again.

Part One

In the first part we need to count and sum all reachable 9s for each 0. At the input parsing stage we are collecting all 0 points to iterate over them later. For each 0, we find all 9s reachable from it through recursive calls to consecutive neighbors of the current point. We store the 9s in a set to avoid counting them multiple times. For each point we store the set of nines in the cache to not recalculate them again:

 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
type HikingMap struct {
    Map        [][]int8
    ZeroPoints []Point[int8]
}

type ParsedInput = HikingMap

...

func PartOne(inp ParsedInput) int {
    defer Track(time.Now(), "PartOne")
    total := 0
    grid, err := NewGridFromSlices(inp.Map)
    if err != nil {
        panic(fmt.Sprintf("creating grid %s", err))
    }
    cache := NewGridWithDefault[map[[2]int]struct{}](grid.Rows, grid.Columns, nil)

    for _, zp := range inp.ZeroPoints {
        total += len(findNines(grid, zp, cache))
    }

    return total
}

func findNines(
    grid *Grid[int8],
    point Point[int8],
    cache *Grid[map[[2]int]struct{}],
) map[[2]int]struct{} {
    if point.Value == 9 {
        return map[[2]int]struct{}{{point.Row, point.Col}: {}}
    }
    if val := cache.Get(point.Row, point.Col).Value; val != nil {
        return val
    }
    nines := make(map[[2]int]struct{})
    for _, neighbor := range grid.GetNeighborsCross(point.Row, point.Col) {
        if neighbor.Value != point.Value+1 {
            continue
        }

        for p := range findNines(grid, neighbor, cache) {
            nines[p] = struct{}{}
        }
    }
    cache.Set(point.Row, point.Col, nines)
    return nines
}

Part Two

Part two is even simpler, in my opinion. We need to find all the distinct paths from 0 to all 9s. We store only the number of ways from the point to 9s in the cache. The rest is the same as in part one, but we don’t need to update the set, which makes it run faster:

 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
func PartTwo(inp ParsedInput) int {
    defer Track(time.Now(), "PartTwo")
    total := 0
    grid, err := NewGridFromSlices(inp.Map)
    if err != nil {
        panic(fmt.Sprintf("creating grid %s", err))
    }

    cache := NewGridWithDefault(grid.Rows, grid.Columns, -1)

    for _, zp := range inp.ZeroPoints {
        total += findDistinctWays(grid, zp, cache)
    }

    return total
}

func findDistinctWays(grid *Grid[int8], point Point[int8], cache *Grid[int]) int {
    // No need to check the cache here, we can just return 1 for 9
    if point.Value == 9 {
        return 1
    }
    cached := cache.Get(point.Row, point.Col).Value
    if cached != -1 {
        return cached
    }

    ways := 0
    for _, neighbor := range grid.GetNeighborsCross(point.Row, point.Col) {
        if neighbor.Value != point.Value+1 {
            continue
        }
        ways += findDistinctWays(grid, neighbor, cache)
    }
    cache.Set(point.Row, point.Col, ways)
    return ways
}

Tags

  • recursion
  • memoization
  • grid
  • pathfinding