Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Coding · Graphs

Grids as graphs

Lesson · 11 min read · Graphs · Traversing a grid

A grid is a graph you never build: cells are nodes, neighbours come from coordinates. Depth-first fills a region; breadth-first measures distance.

A graph that is never written down

A large share of graph interview problems arrive as a grid: a map of land and water, a maze, a board, an image. Each cell is a node, and each cell's neighbours are the cells next to it. Nobody builds an adjacency list for this, because the neighbours of (r, c) can be computed from the coordinates whenever they are needed.

012012or→012012
Left: four neighbours, sharing a side. Right: eight, adding the diagonals. The problem statement says which.

The neighbours are listed with a pair of direction arrays, so one loop handles all of them:

DIRS = [(-1, 0), (0, 1), (1, 0), (0, -1)]          # up, right, down, left

def neighbours(r, c, rows, cols):
    for dr, dc in DIRS:
        nr, nc = r + dr, c + dc
        if 0 <= nr < rows and 0 <= nc < cols:      # in bounds
            yield nr, nc

With R rows and C columns there are n = R·C nodes and at most 4n edges, so every traversal below costs O(R·C).

Three checks before every step

Almost every bug in a grid traversal is one of three checks done late, done wrong, or forgotten. Before moving into a neighbour, confirm that it is:

  1. inside the grid, checked first, before the cell is read;
  2. passable: land rather than water, open rather than a wall, the right colour;
  3. not yet visited.

Visited cells are tracked either in a separate boolean grid or by overwriting the cell in the input (turning land into water once it is counted). Overwriting saves memory and is common in interviews, but it destroys the input; ask before doing it, and do not do it when the caller needs the grid afterwards.

Depth-first search: filling a region

Depth-first search on a grid is the flood fill of a paint program: start in a cell and spread to every connected cell of the same kind. It answers "which cells belong to this region?", "how big is it?" and "does it touch the border?".

Depth-first flood fill

fill(r, c)

  1. If (r, c) is outside the grid, not passable, or visited, return.
  2. Mark (r, c) visited.
  3. Call fill on the four neighbours: up, right, down, left.

Checking at the top of the call, rather than before each call, keeps the code short: every neighbour is passed in and the call itself rejects the bad ones. Below, the search fills the island that contains (0, 0). The numbers give the order in which cells are reached.

Flood fill from (0, 0); neighbours tried up, right, down, left.

  1. Step 1 / 14
    0123401231
    call stack (bottom → top)
    (0,0)0

    Start at (0,0): mark it visited.

  2. Step 2 / 14
    01234012312
    call stack (bottom → top)
    (0,0)0(0,1)1

    Move right to (0,1): it is in bounds, land and unvisited. Mark it (number 2).

  3. Step 3 / 14
    012340123123
    call stack (bottom → top)
    (0,0)0(0,1)1(1,1)2

    Move down to (1,1): it is in bounds, land and unvisited. Mark it (number 3).

  4. Step 4 / 14
    0123401231234
    call stack (bottom → top)
    (0,0)0(0,1)1(1,1)2(1,2)3

    Move right to (1,2): it is in bounds, land and unvisited. Mark it (number 4).

  5. Step 5 / 14
    01234012312345
    call stack (bottom → top)
    (0,0)0(0,1)1(1,1)2(1,2)3(2,2)4

    Move down to (2,2): it is in bounds, land and unvisited. Mark it (number 5).

  6. Step 6 / 14
    012340123123456
    call stack (bottom → top)
    (0,0)0(0,1)1(1,1)2(1,2)3(2,2)4(2,3)5

    Move right to (2,3): it is in bounds, land and unvisited. Mark it (number 6).

  7. Step 7 / 14
    0123401231234567
    call stack (bottom → top)
    (0,0)0(0,1)1(1,1)2(1,2)3(2,2)4(2,3)5(3,3)6

    Move down to (3,3): it is in bounds, land and unvisited. Mark it (number 7).

  8. Step 8 / 14
    0123401231234567
    call stack (bottom → top)
    (0,0)0(0,1)1(1,1)2(1,2)3(2,2)4(2,3)5

    All four neighbours of (3,3) are water, outside the grid or visited: it is finished; back to (2,3).

  9. Step 9 / 14
    0123401231234567
    call stack (bottom → top)
    (0,0)0(0,1)1(1,1)2(1,2)3(2,2)4

    All four neighbours of (2,3) are water, outside the grid or visited: it is finished; back to (2,2).

  10. Step 10 / 14
    0123401231234567
    call stack (bottom → top)
    (0,0)0(0,1)1(1,1)2(1,2)3

    All four neighbours of (2,2) are water, outside the grid or visited: it is finished; back to (1,2).

  11. Step 11 / 14
    0123401231234567
    call stack (bottom → top)
    (0,0)0(0,1)1(1,1)2

    All four neighbours of (1,2) are water, outside the grid or visited: it is finished; back to (1,1).

  12. Step 12 / 14
    0123401231234567
    call stack (bottom → top)
    (0,0)0(0,1)1

    All four neighbours of (1,1) are water, outside the grid or visited: it is finished; back to (0,1).

  13. Step 13 / 14
    0123401231234567
    call stack (bottom → top)
    (0,0)0

    All four neighbours of (0,1) are water, outside the grid or visited: it is finished; back to (0,0).

  14. Step 14 / 14
    0123401231234567
    call stack (bottom → top)
    empty0

    All four neighbours of (0,0) are water, outside the grid or visited: it is finished. The region has 7 cells; the numbers give the order they were reached.

current cellon the call stackfinished

The cell (3, 1) is land but is never reached: it touches the island only diagonally, and these moves do not include diagonals. Counting islands is this fill run from every unvisited land cell; the pattern module on connected components does exactly that.

Breadth-first search: measuring distance

Run breadth-first search from a cell and each ring of the search is one move further away, so the search labels every reachable cell with its shortest distance. This is the method for "fewest moves through a maze", "minutes until every orange rots" and "distance to the nearest exit".

Breadth-first search from (0, 0) through open cells; # is a wall. Each cell shows its distance.

  1. Step 1 / 9
    0123401230
    queue: ring 0
    (0,0)0

    Start at (0,0) with distance 0.

  2. Step 2 / 9
    012340123011
    queue: ring 1
    (0,1)0(1,0)1

    Ring 1: 2 new cells, each one move from a cell of ring 0 and never seen before. Their distance is 1.

  3. Step 3 / 9
    01234012301212
    queue: ring 2
    (0,2)0(2,0)1

    Ring 2: 2 new cells, each one move from a cell of ring 1 and never seen before. Their distance is 2.

  4. Step 4 / 9
    0123401230121323
    queue: ring 3
    (1,2)0(3,0)1

    Ring 3: 2 new cells, each one move from a cell of ring 2 and never seen before. Their distance is 3.

  5. Step 5 / 9
    0123401230121342434
    queue: ring 4
    (1,3)0(2,2)1(3,1)2

    Ring 4: 3 new cells, each one move from a cell of ring 3 and never seen before. Their distance is 4.

  6. Step 6 / 9
    012340123012134524345
    queue: ring 5
    (1,4)0(3,2)1

    Ring 5: 2 new cells, each one move from a cell of ring 4 and never seen before. Their distance is 5.

  7. Step 7 / 9
    01234012301261345246345
    queue: ring 6
    (0,4)0(2,4)1

    Ring 6: 2 new cells, each one move from a cell of ring 5 and never seen before. Their distance is 6.

  8. Step 8 / 9
    012340123012613452463457
    queue: ring 7
    (3,4)0

    Ring 7: 1 new cell, each one move from a cell of ring 6 and never seen before. Their distance is 7.

  9. Step 9 / 9
    012340123012613452463457
    queue: ring 7
    empty0

    No cell can be added: every open cell reachable from (0,0) carries its distance. Cells left blank cannot be reached.

the ring just addedreached earlier

Several starting cells at once (every rotten orange, every exit) need no new algorithm: put them all in the queue at distance 0 before the loop starts. That is multi-source breadth-first search.

Implementation and cost

The flood fill problem at the end of this module is the depth-first fill in its plainest form. Its solution in all six languages:

def flood_fill(image, sr, sc, color):
    old = image[sr][sc]
    if old == color:
        return image                    # nothing to do, and filling would never stop
    rows, cols = len(image), len(image[0])

    def fill(r, c):
        if r < 0 or c < 0 or r >= rows or c >= cols or image[r][c] != old:
            return                      # outside, another colour, or already repainted
        image[r][c] = color
        fill(r - 1, c)
        fill(r, c + 1)
        fill(r + 1, c)
        fill(r, c - 1)

    fill(sr, sc)
    return image

The same code is the reference solution to Repaint a region of an image, where it is compiled and run against the tests in all six languages.

TimeO(R · C)
SpaceO(R · C)

Every cell is visited at most once and examines four neighbours. The recursive fill can go R·C calls deep, a snake-shaped region filling the grid; on a 1,000 × 1,000 grid that is a million frames, so use an explicit stack or breadth-first search when the grid is large.