Skip to content

Multi-source BFS

“For every cell, how far is the nearest zero?” invites a BFS from each cell, which is O((mn)2)O((mn)^2) and hopeless. Reverse the question and it collapses:

Instead of searching from each cell to the nearest source, push every source into the queue at once and let one BFS expand outward from all of them simultaneously.

The first time any wave reaches a cell, it arrived from the nearest source — because BFS expands in non-decreasing distance order and all sources started at distance 0. One pass, O(mn)O(mn), no repeated work.

This is one of those patterns that feels like a trick the first time and obvious forever after.

What you’ll learn

  • Why seeding many sources still yields correct shortest distances.
  • The level loop that converts BFS layers into elapsed time or distance.
  • The two ways to track visited cells — and the “distance array doubles as visited” idiom.
  • How to detect unreachable cells, which is what most of these problems actually test.
  • Three real LeetCode problems solved in the browser: 994, 542, 1162.

The cue

Template 1 — distance to the nearest source

The distance array doubles as the visited marker, which removes a whole category of bugs.

multi_source_distance.py
from collections import deque
 
 
def nearest_source_distance(grid, is_source):
    rows, cols = len(grid), len(grid[0])
    dist = [[-1] * cols for _ in range(rows)]     # -1 means "not yet reached"
    queue = deque()
 
    for r in range(rows):                        # SEED every source at once
        for c in range(cols):
            if is_source(grid[r][c]):
                dist[r][c] = 0
                queue.append((r, c))
 
    while queue:
        r, c = queue.popleft()
        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 dist[nr][nc] == -1:
                dist[nr][nc] = dist[r][c] + 1    # set distance = mark visited
                queue.append((nr, nc))
 
    return dist
multi_source_distance.py
from collections import deque
 
 
def nearest_source_distance(grid, is_source):
    rows, cols = len(grid), len(grid[0])
    dist = [[-1] * cols for _ in range(rows)]     # -1 means "not yet reached"
    queue = deque()
 
    for r in range(rows):                        # SEED every source at once
        for c in range(cols):
            if is_source(grid[r][c]):
                dist[r][c] = 0
                queue.append((r, c))
 
    while queue:
        r, c = queue.popleft()
        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 dist[nr][nc] == -1:
                dist[nr][nc] = dist[r][c] + 1    # set distance = mark visited
                queue.append((nr, nc))
 
    return dist

Template 2 — elapsed time, via level loops

When the answer is “how many minutes/steps”, process the queue one level at a time, exactly as in Tree BFS.

multi_source_levels.py
steps = 0
while queue and still_work_remaining:
    steps += 1
    for _ in range(len(queue)):          # snapshot: exactly this level
        r, c = queue.popleft()
        # ... expand neighbours, appending to the same queue
multi_source_levels.py
steps = 0
while queue and still_work_remaining:
    steps += 1
    for _ in range(len(queue)):          # snapshot: exactly this level
        r, c = queue.popleft()
        # ... expand neighbours, appending to the same queue

The for _ in range(len(queue))for _ in range(len(queue)) snapshot is what separates one time step from the next. Without it every cell would be its own “minute”.

Detecting unreachable cells

This is what most of these problems really test. After the BFS finishes, any cell still holding the initial sentinel was never reached:

  • 994 Rotting Oranges — a fresh orange remains: return -1-1.
  • 542 01 Matrix — guaranteed at least one zero, so everything is reachable.
  • 1162 As Far from Land as Possible — all-water or all-land: return -1-1.

Rather than rescanning the grid at the end, it is usually cleaner to keep a counter of the remaining work (freshfresh in LC 994) and decrement it as you convert cells. Then fresh == 0fresh == 0 is an O(1)O(1) check instead of an O(mn)O(mn) sweep — and it also lets you stop the loop early.

TimeSpace
Multi-source BFSO(mn)O(m \cdot n)O(mn)O(m \cdot n)
BFS once per sourceO((mn)2)O((m \cdot n)^2)O(mn)O(m \cdot n)

The variant map

VariantSourcesAnswer read fromCanonical problem
Time for full spreadAll rotten cellsThe level count, plus a reachability check994 Rotting Oranges
Distance to nearest sourceAll zerosThe whole distdist grid542 01 Matrix
Maximise distance from sourcesAll land cellsThe last level reached1162
Fill from all gatesAll gatesThe distdist grid286 (Premium)
Single source, 8 directionsOne cellLevel count1091
Spread with obstaclesAll sourcesSame, skipping walls286 · 2101

Practice — real LeetCode problems

LC 994 — Rotting Oranges · Medium

Problem. In a grid, 00 is empty, 11 is a fresh orange, 22 is rotten. Each minute, every fresh orange 4-directionally adjacent to a rotten one becomes rotten. Return the minimum minutes until no fresh orange remains, or -1-1 if that is impossible.

Constraints. 1 <= m, n <= 101 <= m, n <= 10, cells are 00, 11 or 22.

Examples. [[2,1,1],[1,1,0],[0,1,1]][[2,1,1],[1,1,0],[0,1,1]] gives 44 · [[2,1,1],[0,1,1],[1,0,1]][[2,1,1],[0,1,1],[1,0,1]] gives -1-1 (the bottom-left 11 is isolated) · [[0,2]][[0,2]] gives 00

Editorial — approach, complexity, follow-ups

All rotten oranges start rotting simultaneously, which is precisely multi-source BFS. Each BFS level is one minute.

Time O(mn)O(m \cdot n). Space O(mn)O(m \cdot n) for the queue.

Three details, each with a test case:

  • fresh == 0fresh == 0 up front returns 00. [[0,2]][[0,2]] and [[0]][[0]] both have nothing to rot. Without this guard the level loop never runs, minutesminutes stays 00, and you happen to get the right answer — but the explicit guard is what makes [[0]][[0]] (no rotten oranges either) obviously correct rather than accidentally so.
  • while queue and freshwhile queue and fresh. Stopping as soon as freshfresh hits 00 avoids counting a final, wasted minute in which nothing rots. Using just while queuewhile queue would over-count by one on some grids, because the last level enqueued has no fresh neighbours left to convert.
  • -1-1 when fresh oranges survive. [[1]][[1]] has a fresh orange and no rotten one, so it is immediately impossible.

The freshfresh counter is doing real work here: it gives an O(1)O(1) termination test and an O(1)O(1) reachability check, replacing a final O(mn)O(mn) rescan.

Follow-ups you should expect: “Which orange rots last?” — record the cell when you set the final level. “8-directional spread?” — extend the direction list. “Oranges rot at different rates?” — edges are no longer uniform, so it becomes Dijkstra. “Why not DFS?” — DFS does not visit in distance order, so levels would be meaningless.

LC 542 — 01 Matrix · Medium

Problem. Given an m x nm x n binary matrix, return a matrix of the same shape where each entry is the distance to the nearest 00, measured in 4-directional steps.

Constraints. 1 <= m, n <= 10^41 <= m, n <= 10^4, m * n <= 10^4m * n <= 10^4, and there is at least one 00.

Examples. [[0,0,0],[0,1,0],[0,0,0]][[0,0,0],[0,1,0],[0,0,0]] gives itself unchanged · [[0,0,0],[0,1,0],[1,1,1]][[0,0,0],[0,1,0],[1,1,1]] gives [[0,0,0],[0,1,0],[1,2,1]][[0,0,0],[0,1,0],[1,2,1]]

Editorial — approach, complexity, follow-ups

Reverse the question. Rather than asking each 11 to find its nearest 00, start from all the zeros and let the wave assign distances as it spreads.

Time O(mn)O(m \cdot n). Space O(mn)O(m \cdot n).

The elegant part is that distdist initialised to -1-1 serves as both the answer and the visited set. One array, two jobs, and no way to enqueue a cell twice.

Writing distances into matmat in place is possible but fiddly, since 00 and 11 are meaningful input values and you would need a sentinel that cannot collide. A separate distdist grid is clearer and the space is O(mn)O(mn) either way.

Follow-ups you should expect: “What if there were no zeros?” — the constraints guarantee one; otherwise every cell stays -1-1 and you would define the answer. “Diagonal distance?” — add the diagonal offsets, giving Chebyshev distance. “Do it with O(1)O(1) extra space?” — the two-pass DP. “Nearest 00 by Manhattan distance ignoring walls?” — no traversal needed, just coordinate arithmetic.

LC 1162 — As Far from Land as Possible · Medium

Problem. Given an n x nn x n grid of 00 (water) and 11 (land), find the water cell whose distance to the nearest land cell is maximised, and return that distance (Manhattan / 4-directional). If no water or no land exists, return -1-1.

Constraints. 1 <= n <= 1001 <= n <= 100, cells are 00 or 11.

Examples. [[1,0,1],[0,0,0],[1,0,1]][[1,0,1],[0,0,0],[1,0,1]] gives 22 (the centre) · [[1,0,0],[0,0,0],[0,0,0]][[1,0,0],[0,0,0],[0,0,0]] gives 44 (the far corner) · [[0,0],[0,0]][[0,0],[0,0]] gives -1-1

Editorial — approach, complexity, follow-ups

Maximising the distance to the nearest land sounds like an optimisation over water cells, but multi-source BFS answers it directly: seed all land, expand, and the last level reached is the farthest any water cell can be.

Time O(n2)O(n^2). Space O(n2)O(n^2).

Two details worth getting right:

  • Initialise dist = -1dist = -1. The first iteration of the loop processes the land cells themselves and increments distdist to 00, correctly recording that land is at distance 0 from land. Starting at 00 over-counts by one.
  • Both degenerate cases return -1-1. not queuenot queue catches all-water; len(queue) == n * nlen(queue) == n * n catches all-land. Checking only one of them fails half the edge tests, and [[0,0],[0,0]][[0,0],[0,0]] / [[1,1],[1,1]][[1,1],[1,1]] cover both.

Note this is the mirror of LC 542. There the answer was the whole distance grid; here it is a single number, the maximum — which BFS hands you for free as the final level, with no need to store distances at all.

Follow-ups you should expect: “Which cell is farthest?” — record a cell when you enter the final level. “Chebyshev distance instead of Manhattan?” — use 8 directions. “Multiple such cells?” — collect all cells in the last level. “Do it without BFS?” — the two-pass DP from LC 542 computes the distance grid, then take its maximum.

LeetCode problem set

#ProblemDifficultyThe twist
994Rotting OrangesMediumLevels are minutes; a freshfresh counter gives O(1)O(1) termination and reachability
54201 MatrixMediumThe distdist grid doubles as the visited set; a two-pass DP also works
1162As Far from Land as PossibleMediumThe answer is the last level; guard all-land and all-water
1091Shortest Path in Binary MatrixMediumSingle source, but 8 directions — plain BFS
286Walls and GatesMedium · PremiumSeed all gates, skip walls — the canonical statement of this pattern

Interview follow-ups

They askWhat they’re checkingThe answer
“Why is seeding all sources correct?”Whether you understand BFS’s invariantIt is BFS from a virtual super-source joined to every source by a 0-cost edge; the queue stays ordered by distance, so first visit is shortest
“Why mark visited on enqueue?”The critical habitOtherwise several neighbours enqueue the same cell before it is processed, bloating the queue and risking a later, larger distance
“How do you know when it is impossible?”RigourKeep a counter of remaining work; a non-zero counter afterwards means unreachable cells
“Why not DFS?”Deliberate choiceDFS does not visit in distance order, so levels and shortest distances are meaningless
“What if edges had different costs?”The boundaryMulti-source BFS needs uniform cost; otherwise use Dijkstra with all sources pushed at distance 0
“Can you do LC 542 in O(1)O(1) space?”BreadthYes — the two-pass DP sweeping top-left then bottom-right
“Why dist = -1dist = -1 initially in 1162?”Attention to off-by-oneThe first level processed is the land itself, which must register as distance 0

Edge-case checklist

  • No sources at all — LC 994 with only fresh oranges gives -1-1; LC 1162 with no land gives -1-1.
  • No targets at all — LC 994 with no fresh oranges gives 00; LC 1162 with no water gives -1-1. Both degenerate cases need guards.
  • Everything is a source — the queue starts full and no level ever expands.
  • Unreachable cells — an isolated fresh orange behind empty cells; the reason for the counter check.
  • 1×1 grid — every variant must survive it.
  • Off-by-one in the level counter — initialise -1-1 when the source level counts as distance 0, and 00 when counting elapsed steps.
  • One wasted final minute (LC 994) — while queue and freshwhile queue and fresh rather than while queuewhile queue.
  • Duplicate enqueues — guaranteed if you mark on dequeue instead of enqueue.

Recap

  • Seed the queue with every source and run one BFS. Correct because it is BFS from a virtual super-source, so the first visit to a cell is its shortest distance.
  • Turns O((mn)2)O((mn)^2) into O(mn)O(mn). If you are about to loop “BFS per source”, invert it.
  • Mark visited on enqueue, never on dequeue — with many sources, duplicate enqueues are otherwise guaranteed.
  • A distance array initialised to -1-1 is both the answer and the visited set.
  • For elapsed time, process one level per step with a len(queue)len(queue) snapshot.
  • Track remaining work with a counter for O(1)O(1) termination and unreachability checks.
  • Requires uniform edge cost; weighted versions are Dijkstra (with all sources pushed at distance 0).

Next: Union-Find Problem Patterns — the other way to answer connectivity questions, and the one that handles edges arriving over time.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did