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.

  • 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.

Multi-source BFS is one BFS with several starting points seeded at once — not one search per source. Watch every rotten orange enter the queue before the loop begins:

gridEvery source seeds the queue, and the answer is a level countLC 994 · O(rows x cols)
rotten front
0,0
minute0fresh left8
sources1fresh8
setup1 orange is already rotten, and **all of them** seed the queue at once. That is the multi-source trick: several starting points in one BFS, which spreads from all of them simultaneously rather than needing one search per source.
1/8

Processing the queue one level at a time is what makes the answer a number of minutes rather than a number of oranges — the same level-snapshot trick as tree BFS. And note the final check for unreachable fresh oranges, which is the part most submissions miss.

Template 1 — distance to the nearest source

Section titled “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

Template 2 — elapsed time, via level loops

Section titled “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

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

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.
  • 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.

Rather than rescanning the grid at the end, it is usually cleaner to keep a counter of the remaining work (fresh in LC 994) and decrement it as you convert cells. Then fresh == 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.

ApproachTimeSpace
Multi-source BFSO(mn)O(m \cdot n)O(mn)O(m \cdot n)
BFS once per source, keeping the minimumO((mn)2)O((m \cdot n)^2)O(mn)O(m \cdot n)
Dijkstra from a virtual super-sourceO(mnlogmn)O(mn \log mn)O(mn)O(m \cdot n)

Every cell is enqueued at most once — that is what marking on enqueue buys — so the queue does O(mn)O(mn) pops and each pop looks at 4 neighbours. The number of sources does not appear in the bound at all: seeding 1 cell and seeding 10,000 cells cost the same, because the total work is one pass over the grid either way.

That is the whole argument for the pattern. With kk sources the naive approach is kk independent BFS runs, so on a 100×100100 \times 100 grid where half the cells are sources it is 5,000 traversals of 10,000 cells — 5×1075 \times 10^7 against 10410^4.

LC 542, grid = [[0,0,0],[0,1,0],[1,1,1]]. Four zeros seed the queue at once, so the first wave already comes from four different directions.

Seed: dist is 0 at (0,0), (0,1), (0,2), (1,0), (1,2); -1 elsewhere. Queue holds those five cells.

wavecells reachedset towhy
1(1,1), (2,0), (2,2)1(1,1) is reached by whichever of its four zero-neighbours pops first — it does not matter which, they are all at distance 0
2(2,1)2its only unvisited-at-wave-1 neighbours were (2,0) and (2,2), both at distance 1

Final dist:

text
0 0 0
0 1 0
1 2 1

Three things the trace shows that the code hides:

  • (1,1) is contended and it does not matter. Four cells at distance 0 all want to claim it. Whichever pops first writes 1, and the dist[nr][nc] == -1 guard makes the other three no-ops. This is why marking on enqueue matters more here than in single-source BFS: with many sources, contention is the normal case, not an edge case.
  • The queue never holds a cell twice, so it never exceeds O(mn)O(mn). Marking on dequeue instead would let (1,1) sit in the queue four times, and on a grid that is mostly sources the queue degenerates toward 4mn4mn entries.
  • Wave number = distance. The waves are only visible if you use the level loop; Template 1 gets the same numbers without it, because dist[r][c] + 1 carries the level in the data instead of the control flow.

LC 994, grid = [[2,1,1],[1,1,0],[0,1,1]], with the fresh counter — the same walk read as elapsed time:

minuterotsfresh left
1(1,0), (0,1)4
2(1,1), (0,2)2
3(2,1)1
4(2,2)0

Answer 4. Note the loop condition while queue and fresh: at minute 4 the last orange rots and fresh hits 0, so the loop stops without a fifth iteration. Drop the and fresh and the count becomes 5 — the classic off-by-one in this problem, caused by counting a minute in which nothing changed.

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

Problem. In a grid, 0 is empty, 1 is a fresh orange, 2 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 if that is impossible.

Constraints. 1 <= m, n <= 10, cells are 0, 1 or 2.

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

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

The fresh 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.

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

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

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

Editorial — approach, complexity, follow-ups

Reverse the question. Rather than asking each 1 to find its nearest 0, 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 dist initialised to -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 mat in place is possible but fiddly, since 0 and 1 are meaningful input values and you would need a sentinel that cannot collide. A separate dist 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 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 0 by Manhattan distance ignoring walls?” — no traversal needed, just coordinate arithmetic.

LC 1162 — As Far from Land as Possible · Medium

Section titled “LC 1162 — As Far from Land as Possible · Medium”

Problem. Given an n x n grid of 0 (water) and 1 (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.

Constraints. 1 <= n <= 100, cells are 0 or 1.

Examples. [[1,0,1],[0,0,0],[1,0,1]] gives 2 (the centre) · [[1,0,0],[0,0,0],[0,0,0]] gives 4 (the far corner) · [[0,0],[0,0]] gives -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 = -1. The first iteration of the loop processes the land cells themselves and increments dist to 0, correctly recording that land is at distance 0 from land. Starting at 0 over-counts by one.
  • Both degenerate cases return -1. not queue catches all-water; len(queue) == n * n catches all-land. Checking only one of them fails half the edge tests, and [[0,0],[0,0]] / [[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.

Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.

5 problems
0 easy5 medium0 hard

Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.

  • 994Rotting OrangesmediumLevels are minutes; a `fresh` counter gives $O(1)$ termination and reachabilityNeetCode 150amazonmicrosoftbytedance
  • 286Walls and GatespremiummediumSeed all gates, skip walls -- the canonical statement of this patternNeetCode 150
  • 54201 MatrixmediumThe `dist` grid doubles as the visited set; a two-pass DP also works
  • 1091Shortest Path in Binary MatrixmediumSingle source, but 8 directions -- plain BFS
  • 1162As Far from Land as PossiblemediumThe answer is the **last** level; guard all-land *and* all-water
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 = -1 initially in 1162?”Attention to off-by-oneThe first level processed is the land itself, which must register as distance 0
  • No sources at all — LC 994 with only fresh oranges gives -1; LC 1162 with no land gives -1.
  • No targets at all — LC 994 with no fresh oranges gives 0; LC 1162 with no water gives -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 when the source level counts as distance 0, and 0 when counting elapsed steps.
  • One wasted final minute (LC 994) — while queue and fresh rather than while queue.
  • Duplicate enqueues — guaranteed if you mark on dequeue instead of enqueue.
pch.quizTag Multi-source BFS — self-check
  1. Why is seeding every source into one queue correct, rather than an approximation?

    pch.quizShowAnswer

    B — Because it is single-source BFS on the same graph plus a virtual super-source joined to every source at distance 0 — so the standard first-visit-is-shortest argument applies unchanged — The super-source is never built — seeding the queue is the same thing — but naming it is the cleanest justification, and it also explains why the source count does not appear in the complexity.

  2. How does the running time depend on the number of sources k?

    pch.quizShowAnswer

    B — It does not — every cell is enqueued at most once, so the total is O(mn) whether there is 1 source or 10,000 — This is the whole point of the pattern. BFS-per-source is O(k·mn), which on a grid that is half sources is thousands of full traversals instead of one.

  3. You mark cells visited when you dequeue them instead of when you enqueue them. What happens?

    pch.quizShowAnswer

    B — The same cell gets enqueued by several sources or neighbours before being processed, so the queue bloats toward O(mn) duplicates and a cell can take its distance from a later wave — With one source this is a mild inefficiency; with many sources contention is the normal case. In the dry run above, cell (1,1) is wanted by four zero-cells at once.

  4. In LC 994 the loop is `while queue and fresh`. Why is the `and fresh` clause there?

    pch.quizShowAnswer

    B — Because without it the last wave — which rots the final orange and then finds nothing more to do — still increments the minute counter, giving an answer one too large — The dry run ends at minute 4 with fresh == 0. Looping again would count a fifth minute in which nothing changed. Tracking remaining work as a counter also makes the unreachable check O(1) instead of a final O(mn) sweep.

  5. The grid gains per-cell traversal costs of 1 or 3. Does multi-source BFS still work?

    pch.quizShowAnswer

    B — No — BFS requires uniform edge cost. Push every source at distance 0 into a heap and run Dijkstra instead (or a deque, if the costs are only 0 and 1) — The multi-source *idea* survives every weighting — seed all sources at 0 — but the structure has to match the weights: queue for uniform, deque for 0/1, heap for arbitrary.

  • Cue — “distance to the nearest X” or “time for X to spread everywhere”, with many starting cells. If you are about to write “BFS from each source”, stop.
  • Do — push all sources into the queue with distance 0, then run one ordinary BFS.
  • Why it is correct — identical to BFS from a virtual super-source joined to every source by a 0-cost edge.
  • Mark on enqueue, never on dequeue. dist[r][c] = -1 doubles as the visited set.
  • Elapsed time — level loop with a for _ in range(len(queue)) snapshot.
  • Unreachable — keep a counter of remaining work; fresh > 0 at the end means -1. Loop while queue and fresh to avoid counting a dead final minute.
  • CostO(mn)O(mn) time and space, independent of the source count. Weighted variants: deque for 0/1 costs, heap for arbitrary.
  • 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 is both the answer and the visited set.
  • For elapsed time, process one level per step with a 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.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading