Skip to content
CCC Python Course

BFS and flood fill (iterative)

Module
M4.13
Lesson
1 of 1
Reading time
5 min

In this lesson

  • Explain why a queue makes breadth-first search find shortest paths.
  • Write an iterative BFS that finds the fewest moves across a grid.
  • Use flood fill to count connected regions and measure their sizes.
  • Replace deep recursion with a deque so large grids do not crash.

Picture a grid of open cells and walls. You start at S and want to reach E, moving one cell up, down, left or right at a time. What is the fewest number of moves?

Every move costs the same, one step. In that situation, breadth-first searchA graph traversal algorithm that explores vertices level by level in order of distance from the source.In the glossary (BFS) finds the answer. It explores the grid in rings. First it finds every cell one move away, then every cell two moves away, and so on. The first time a ring touches E, the ring number is the answer.

Rings of distance

Take the start cell and give it distance 0. Its open neighbours get distance 1. Their open neighbours that have no distance yet get distance 2. Keep going until you reach E or run out of cells.

Each cell gets a distance once, the first time the search reaches it. That first time is always the shortest, because every cell in an earlier ring was found before any cell in a later ring.

Doing this by hand is easy on paper. The program needs a way to remember which cells are waiting to be explored, in the right order. That is the job of a queueA first-in, first-out data structure where elements are added at the back and removed from the front.In the glossary.

The queue keeps the rings in order

A queue works like a line at a counter. New items join at the back. The next item to be served leaves from the front. First in, first out.

BFS starts with only the start cell in the queue. Then it repeats one step: take the cell at the front, look at its four neighbours, and add each new open neighbour to the back with a distance one higher.

This keeps a useful promise. At every moment, the distances in the queue never go down from front to back, and they differ by at most one. All the distance-3 cells leave the queue before the first distance-4 cell does. That is why the rings come out in order.

Step through the search below. Try the three presets. The "No path" grid shows what happens when the walls cut E off completely.

Input
Grid
Grid, step 1 of 150E012340123
Queue
Queue, step 1 of 150,0front, back
Speed
The search starts at S: its distance is 0 and it is the only cell in the queue.
  • Current
  • Queued
  • Done
  • Not reached
  • Answer path
  • Wall

Figure 1Breadth-first search expanding level by level across a grid

Read the steps as text

Breadth-first search on a small grid. Starting from S, the cell at the front of the queue becomes current and its open neighbours join the back of the queue with a distance one larger. When E comes off the queue, one shortest path is marked; when the queue runs empty first, there is no path.

Open grid

  1. The search starts at S: its distance is 0 and it is the only cell in the queue.
  2. Cell 0,0 leaves the front of the queue and becomes current (distance 0). Its open neighbours not seen before, 1,0 and 0,1, join the back of the queue with distance 1.
  3. Cell 1,0 leaves the front of the queue and becomes current (distance 1). Its open neighbours not seen before, 2,0 and 1,1, join the back of the queue with distance 2.
  4. Cell 0,1 leaves the front of the queue and becomes current (distance 1). Its open neighbours not seen before, 0,2, join the back of the queue with distance 2.
  5. Cell 2,0 leaves the front of the queue and becomes current (distance 2). Its open neighbours not seen before, 2,1, join the back of the queue with distance 3.
  6. Cell 1,1 leaves the front of the queue and becomes current (distance 2). Its open neighbours not seen before, 1,2, join the back of the queue with distance 3.
  7. Cell 0,2 leaves the front of the queue and becomes current (distance 2). Its open neighbours not seen before, 0,3, join the back of the queue with distance 3.
  8. Cell 2,1 leaves the front of the queue and becomes current (distance 3). Its open neighbours not seen before, 2,2, join the back of the queue with distance 4.
  9. Cell 1,2 leaves the front of the queue and becomes current (distance 3). Its open neighbours not seen before, 1,3, join the back of the queue with distance 4.
  10. Cell 0,3 leaves the front of the queue and becomes current (distance 3). It has no open neighbour that is not already seen, so nothing joins the queue.
  11. Cell 2,2 leaves the front of the queue and becomes current (distance 4). Its open neighbours not seen before, 2,3, join the back of the queue with distance 5.
  12. Cell 1,3 leaves the front of the queue and becomes current (distance 4). It has no open neighbour that is not already seen, so nothing joins the queue.
  13. E comes off the front of the queue with distance 5. Every cell nearer to S was taken out before it, so 5 is the shortest distance.
  14. Walking back from E to the cell each one was reached from gives one shortest path, 5 moves long.

Walls

  1. The search starts at S: its distance is 0 and it is the only cell in the queue.
  2. Cell 0,0 leaves the front of the queue and becomes current (distance 0). Its open neighbours not seen before, 1,0 and 0,1, join the back of the queue with distance 1.
  3. Cell 1,0 leaves the front of the queue and becomes current (distance 1). Its open neighbours not seen before, 2,0 and 1,1, join the back of the queue with distance 2.
  4. Cell 0,1 leaves the front of the queue and becomes current (distance 1). It has no open neighbour that is not already seen, so nothing joins the queue.
  5. Cell 2,0 leaves the front of the queue and becomes current (distance 2). Its open neighbours not seen before, 3,0 and 2,1, join the back of the queue with distance 3.
  6. Cell 1,1 leaves the front of the queue and becomes current (distance 2). It has no open neighbour that is not already seen, so nothing joins the queue.
  7. Cell 3,0 leaves the front of the queue and becomes current (distance 3). It has no open neighbour that is not already seen, so nothing joins the queue.
  8. Cell 2,1 leaves the front of the queue and becomes current (distance 3). Its open neighbours not seen before, 2,2, join the back of the queue with distance 4.
  9. Cell 2,2 leaves the front of the queue and becomes current (distance 4). Its open neighbours not seen before, 3,2 and 2,3, join the back of the queue with distance 5.
  10. Cell 3,2 leaves the front of the queue and becomes current (distance 5). Its open neighbours not seen before, 3,3, join the back of the queue with distance 6.
  11. Cell 2,3 leaves the front of the queue and becomes current (distance 5). Its open neighbours not seen before, 1,3 and 2,4, join the back of the queue with distance 6.
  12. Cell 3,3 leaves the front of the queue and becomes current (distance 6). Its open neighbours not seen before, 3,4, join the back of the queue with distance 7.
  13. Cell 1,3 leaves the front of the queue and becomes current (distance 6). Its open neighbours not seen before, 0,3, join the back of the queue with distance 7.
  14. E comes off the front of the queue with distance 6. Every cell nearer to S was taken out before it, so 6 is the shortest distance.
  15. Walking back from E to the cell each one was reached from gives one shortest path, 6 moves long.

No path

  1. The search starts at S: its distance is 0 and it is the only cell in the queue.
  2. Cell 0,0 leaves the front of the queue and becomes current (distance 0). Its open neighbours not seen before, 1,0 and 0,1, join the back of the queue with distance 1.
  3. Cell 1,0 leaves the front of the queue and becomes current (distance 1). Its open neighbours not seen before, 2,0 and 1,1, join the back of the queue with distance 2.
  4. Cell 0,1 leaves the front of the queue and becomes current (distance 1). It has no open neighbour that is not already seen, so nothing joins the queue.
  5. Cell 2,0 leaves the front of the queue and becomes current (distance 2). It has no open neighbour that is not already seen, so nothing joins the queue.
  6. Cell 1,1 leaves the front of the queue and becomes current (distance 2). It has no open neighbour that is not already seen, so nothing joins the queue.
  7. The queue is empty and E was never taken out of it: the walls cut it off, so no path exists and the program prints -1.

Use a deque, not a list

A Python list can act as a queue, but removing from its front is slow. pop(0) shifts every remaining item one place to the left. The longer the queue, the more each removal costs, and a big search pays that cost once for every cell.

The deque type from the collections module is built for this. Adding to the back with append() and removing from the front with popleft() both take constant time, no matter how long the queue is.

Python
from collections import deque
queue = deque()queue.append((0, 0))queue.append((0, 1))r, c = queue.popleft()print(r, c)

This prints 0 0, the first cell added. Each item here is a (row, column) pair.

A BFS program for the grid

Here is the whole search. The first input line gives the number of rows and columns, and the grid follows. The program prints the fewest moves from S to E, or -1 if E cannot be reached.

examples/bfs_grid.py
from collections import deque
rows, cols = map(int, input().split())grid = [input().strip() for _ in range(rows)]
start = (0, 0)for r in range(rows):    for c in range(cols):        if grid[r][c] == "S":            start = (r, c)
dist = [[-1] * cols for _ in range(rows)]dist[start[0]][start[1]] = 0queue = deque([start])
answer = -1while queue:    r, c = queue.popleft()    if grid[r][c] == "E":        answer = dist[r][c]        break    for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):        nr, nc = r + dr, c + dc        if (            0 <= nr < rows            and 0 <= nc < cols            and grid[nr][nc] != "#"            and dist[nr][nc] == -1        ):            dist[nr][nc] = dist[r][c] + 1            queue.append((nr, nc))
print(answer)

Input

4 5
S.#..
..#.#
....E
.#...

Output

6
Iterative BFS on an unweighted grid

The dist grid does two jobs. A value of -1 means the search has not reached that cell yet. Any other value is the cell's distance from S.

Look at where a cell gets its distance. The program sets dist[nr][nc] at the moment the cell joins the queue, not when it leaves. If it waited until the cell left, two different neighbours could each find the cell unmarked and add it. The queue would fill up with copies.

The if checks four things before a neighbour joins the queue. The row and column must be inside the grid, the cell must not be a wall, and it must not have a distance yet. The order matters: the bounds checks come first, so the program never reads a cell outside the grid.

Flood fill

flood fillAn algorithm that identifies and visits all connected cells matching a condition in a 2D grid.In the glossary uses the same queue to answer a different question. Which cells are connected to this one?

Picture a map where L marks land and . marks water. Two land cells belong to the same island if you can walk from one to the other over land. To count the islands, scan the grid cell by cell. Each time you find land that no earlier search has reached, start a new search there. That search marks its whole island as seen, so the scan will not count it again.

examples/regions.py
from collections import deque
rows, cols = map(int, input().split())grid = [input() for _ in range(rows)]
seen = [[False] * cols for _ in range(rows)]sizes = []for r in range(rows):    for c in range(cols):        if grid[r][c] != "L" or seen[r][c]:            continue        # (r, c) is land that no earlier search reached: a new region starts here.        seen[r][c] = True        queue = deque([(r, c)])        size = 0        while queue:            cr, cc = queue.popleft()            size += 1            for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):                nr, nc = cr + dr, cc + dc                if (                    0 <= nr < rows                    and 0 <= nc < cols                    and grid[nr][nc] == "L"                    and not seen[nr][nc]                ):                    seen[nr][nc] = True                    queue.append((nr, nc))        sizes.append(size)
print(len(sizes))print(*sorted(sizes))

Input

5 6
LL..L.
L...LL
..L...
.LLL..
......

Output

3
3 3 4
Counting regions of land and measuring each one

The search here has no target cell. It runs until the queue is empty, which means it has reached every land cell in the region. Counting how many cells leave the queue gives the region's size. You can collect other facts the same way, such as a total or a largest value, by updating them each time a cell leaves the queue.

The output says there are three islands, with sizes 3, 3 and 4.

Why not a recursive fill?

You may have seen flood fill written as a function that calls itself on each neighbour. It is short and it works on small grids. On large ones it crashes.

Each call waits on the call stack until the calls it made finish. A long, snaking region makes the chain of waiting calls very long. Python limits how deep that chain can go, and PyPy stopped this program after about 1,400 calls.

examples/deep_recursion.py
def fill(c):    seen[c] = True    if c + 1 < n and not seen[c + 1]:        fill(c + 1)

n = 3000seen = [False] * nfill(0)print("done")

Error: RecursionError

Traceback (most recent call last):
  File "deep_recursion.py", line 9, in <module>
    fill(0)
  File "deep_recursion.py", line 4, in fill
    fill(c + 1)
  File "deep_recursion.py", line 4, in fill
    fill(c + 1)
  File "deep_recursion.py", line 4, in fill
    fill(c + 1)
  [Previous line repeated 1395 more times]
  File "deep_recursion.py", line 3, in fill
    if c + 1 < n and not seen[c + 1]:
RecursionError: maximum recursion depth exceeded
A recursive fill on a single row of 3,000 cells

A contest grid can hold hundreds of thousands of cells, so a recursive fill can fail on a valid input. The queue version keeps its waiting cells in a deque instead of on the call stack. A deque can hold millions of items, so the same search finishes without trouble.

When every move in a grid or graph costs the same, reach for this pattern. Use a queue and a dist or seen grid, and mark each neighbour when it joins the queue.

Practice

Try these on the judges. Each link opens the problem on WMOJ or DMOJ.

  1. 2024 J5
    Harvest Waterloo (opens on WMOJ in a new tab) WMOJ

    Add up what a farmer can harvest from one patch of a field.

  2. 2020 S2
    Escape Room (opens on DMOJ in a new tab) DMOJ

    Decide whether you can escape a grid of numbered rooms.

    Why DMOJ: An older grid reachability problem, good extra practice with a queue.

  3. 2018 J5
    Choose your own path (opens on DMOJ in a new tab) DMOJ

    Check a choose-your-own-adventure book, page by page.

    Why DMOJ: An older problem where the search runs over pages instead of grid cells.