Skip to content

Day 18. Ram run

We are inside a computer now! User wrote strange program, so now bytes are falling on the RAM block we are standing on. We need to avoid them and reach the end of the block.

Part One

In the first part we just need to find the shortest path from the start to the end when only the first kilobyte (1024) has fallen. Each fallen byte is a wall on the grid. Start is at (0, 0) and end is at (70, 70).

First we need to parse the input and prepare the grid, setting cells blocked by fallen bytes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type FallingBytes []Point[rune]

type ParsedInput = FallingBytes
...


func prepareStartGrid(fallingBytes FallingBytes, size int, timeFrame int) *Grid[rune] {
    grid := NewGridWithDefault(size, size, EmptyChar)
    for _, p := range fallingBytes[:timeFrame] {
        grid.Set(p.Row, p.Col, p.Value)
    }
    return grid
}

With the shortest path search we can implement Dijkstra's algorithm or A* (a-star) algorithm which is an optimization for the former. But Dijkstra on the equally weighted grid is the same as BFS. A* would do a minor optimization by using a heuristic metric to prioritize steps closer to the end point.

To have them go we would need the priority queue (or a heap).

We don't need full routing table for the path, so we can just work with distances, you can think of it as DFS with a priority queue instead of a stack:

 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
func FindShortestPath(g *Grid[rune], less func(a, b Step) bool) int {
    unknownStep := Step{Row: -1, Col: -1, StpCnt: -1, Metric: -1}
    visited := NewGridWithDefault(g.Rows, g.Columns, unknownStep)
    endRow, endCol := g.Rows-1, g.Columns-1

    startStep := NewStartStep(g)
    queue := NewHeap[Step](less, 200)
    queue.Push(startStep)

    for queue.Len() > 0 {
        step := queue.Pop()
        // Finish condition
        if step.Row == endRow && step.Col == endCol {
            visited.Set(step.Row, step.Col, step)
            break
        }

        // Do not visit already visited steps, by design we've reach each point
        // with the shortest path
        knownStep := visited.Get(step.Row, step.Col).Value
        if knownStep.StpCnt != -1 {
            continue
        }

        // Mark step as visited, and find next steps
        visited.Set(step.Row, step.Col, step)
        nextSteps := findNextSteps(g, step, visited)

        // Update queue
        for _, nextStep := range nextSteps {
            queue.Push(nextStep)
        }
    }

    // In the end we have the shortest path set to the end point.
    // -1 means that the path is not found
    return visited.Get(endRow, endCol).Value.StpCnt
}

Steps prioritazation logic is based on the steps count from the start point. With A* we also add heuristic metric to the steps count. I've used Manhattan distance to the end point, cause it's fine for the grid without diagonal movement:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Step struct {
    Row, Col int
    StpCnt   int
    Metric   int
}

func NewStartStep(g *Grid[rune]) Step {
    return Step{
        Row:    0,
        Col:    0,
        StpCnt: 0,
        Metric: manhattanToEnd(0, 0, g),
    }
}

// Specific less functions for the priority queue to choose
// the next step to visit. Used function determines the algorithm.
func lessForDijkstra(a, b Step) bool {
    return a.StpCnt < b.StpCnt
}

func lessForAStar(a, b Step) bool {
    return a.StpCnt+a.Metric < b.StpCnt+b.Metric
}

Next steps logic is simple using grid and we just need to properly set new step metric. We also can skip already visited steps here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func findNextSteps(g *Grid[rune], step Step, visited *Grid[Step]) []Step {
    steps := make([]Step, 0, 4)
    for _, neighbor := range g.GetNeighborsCross(step.Row, step.Col) {
        if neighbor.Value == ByteChar {
            continue
        }
        if visited.Get(neighbor.Row, neighbor.Col).Value.StpCnt != -1 {
            continue
        }
        row, col, sCount := neighbor.Row, neighbor.Col, step.StpCnt+1
        // Without diagonal movement we can use Manhattan distance as A* heuristic
        metric := manhattanToEnd(row, col, g)
        newStep := Step{Row: row, Col: col, StpCnt: sCount, Metric: metric}
        steps = append(steps, newStep)

    }
    return steps
}

Answer is the shortest path step count:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const (
    TestGridSize  = 7
    TestTimeFrame = 12
    GridSize      = 71
    TimeFrame     = 1024
)
...

func PartOne(inp ParsedInput) int {
    size, tf := GridSize, TimeFrame

    grid := prepareStartGrid(inp, size, tf)

    // We use A* algorithm to find the shortest path, you can switch to Dijkstra
    // by changing the less function
    return FindShortestPath(grid, lessForAStar)
}

Part Two

In part two we want to know at which time and space point (of falling byte) our path would be fully blocked. We need to find the time frame when the path is blocked.

Naive approach

We can do it by adding the falling bytes after initial ones and check if the path is blocked at each time frame:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// We can skip bytes until the time frame, cause we know they are not blocking anything
// from part one. Also grid already contains them, so we don't need to set them again.
func iterativeBlockerSearch(g *Grid[rune], bytes FallingBytes, tf int) Point[rune] {
    for i, p := range bytes[tf+1:] {
        g.Set(p.Row, p.Col, p.Value)
        if isBlocked(g) {
            fmt.Printf("Blocked at %d: %d,%d\n", tf+i, p.Col, p.Row)
            return p
        }
    }
    panic("No blocker found")
}

func isBlocked(g *Grid[rune]) bool {
    return FindShortestPath(g, lessForAStar) == -1
}

It works but the result calculation takes around 2-3 seconds.

Greedy A*

In this part we don't need to know the shortest path. Our goal is to determine if any path is possible. If we set our comparison function to check only A* heuristic metric we can find some (non-optimal) path quite fast:

1
2
3
4
5
6
7
func isBlocked(g *Grid[rune]) bool {
    return FindShortestPath(g, lessForGreedyAStar) == -1
}

func lessForGreedyAStar(a, b Step) bool {
    return a.Metric < b.Metric
}

This will speed up the search to 0.5-1 second.

But we can do even better. We know the full path is blocked at some point, we also know all the bytes that are falling. We can use binary search to find the exact time frame when the path is blocked:

 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
// This is a binary search implementation to find the blocker:
// https://en.wikipedia.org/wiki/Binary_search
// The idea is to block cells by clusters to find the blocker by log(n) steps.
// We also don't need to check starting timeframe bytes
func bisectBlockerSearch(g *Grid[rune], bytes FallingBytes, tf int) Point[rune] {
    start, end := tf+1, len(bytes)
    for start < end {
        // When the difference is 1 we are at the exact blocker point
        if end-start == 1 {
            fmt.Printf("Blocked at %d: %d,%d\n", start, bytes[start].Col, bytes[start].Row)
            return bytes[start]
        }
        mid := (start + end) / 2
        blockCells(g, bytes[start:mid])
        // This means we can block further aka we need to move mid to right
        if !isBlocked(g) {
            start = mid
            continue
        }
        // Otherwise we move mid to left and end to previous mid (bisect)
        // and free the cells from new mid to new end
        end = mid
        futureMid := (start + end) / 2
        freeCells(g, bytes[futureMid:end])
    }

    panic("No blocker found")
}


func blockCells(g *Grid[rune], toBlock FallingBytes) {
    for _, p := range toBlock {
        g.Set(p.Row, p.Col, p.Value)
    }
}

func freeCells(g *Grid[rune], toFree FallingBytes) {
    for _, p := range toFree {
        g.Set(p.Row, p.Col, EmptyChar)
    }
}

This optimization drops us even further to 2-4 ms.

Tags

  • grid
  • a-star
  • binary search