Multi-Source BFS

One BFS, many starting points. Seed the queue with every source at distance 0 and expand in layers: the first time a cell is reached gives its shortest distance to the nearest source — all of them in a single O(V+E)sweep instead of one BFS per source. Reverse it (seed from the borders) and the same trick answers “which cells can reach the edge?”

Technique5 problems
The unlock

A BFS frontier doesn't care whether it starts from one cell or a hundred. Drop all your sources into the queue at distance 0, then expand exactly as you always would. Because the rings from every source grow in lockstep, the firsttime the frontier touches a cell, the layer number is that cell's distance to the nearestsource — and you got every cell's answer in one sweep.

MENTAL MODEL Many seeds, one frontier — layer = distance

Ordinary BFS drops a pebble in a pond at one point and watches the ripple expand. Each ripple (layer) is one step farther from the source, so the layer a cell lands on is its distance from that source. Multi-source BFS drops many pebbles at the same instant. The ripples expand together and merge; wherever two ripples would collide, the cell was already claimed by the closer pebble. So the layer a cell is first reached on is its distance to whichever source is nearest.

Single-source BFS         vs        Multi-source BFS

   queue = [S]                       queue = [S0, S1, S2, ...]
        │                                  │
   ring grows from ONE center        rings grow from EVERY center
        ▼                                  ▼
   dist = steps from S               dist = steps from the NEAREST S

The trick: a BFS frontier doesn't care how many cells it starts with.
Seed it with k sources at level 0 and the first time it touches a cell,
that cell's level is its distance to whichever source was closest.
The reframe → “distance to the nearest of many sources” is not k separate BFS problems. It is one BFS with k seeds. That collapses O(k·(V+E)) down to a single O(V+E).

SEE IT Watch four corners flood inward and meet

Seed every source at distance 0, then let the shared frontier expand one ring at a time. The cell in the middle is reached from four directions at once — its value is the distance to the nearest corner, not the total:

Multi-source BFS — many seeds share ONE frontier.

  start: seed the queue with ALL sources at distance 0

     0 . 0          queue = [(0,0)=0, (0,2)=0, (2,0)=0, (2,2)=0]
     . . .          (every "0" enters at once, before any expansion)
     0 . 0

  layer 1: every cell one step from ANY source

     0 1 0          the four corners' rings overlap and meet;
     1 . 1          each cell takes whichever source reaches it FIRST
     0 1 0

  layer 2: the last cell, reached from four sides at distance 2

     0 1 0
     1 2 1          (1,1) = 2 — its distance to the NEAREST source
     0 1 0

One sweep, O(V·E). Running a separate BFS per source would be O(sources·V·E).
The tell → step the Visualizetab. Notice that no cell's number ever changes after it is first written. That is the whole correctness argument: first arrival wins, and BFS arrives in distance order.

SAY IT First arrival wins — so mark visited on enqueue

Say it before you write the loop: “The first time the frontier reaches a cell, that distance is final.” BFS expands strictly outward, so the first source to touch a cell is the nearest one. The instant you stamp a distance, you must lock the cell (mark it visited) so a farther source arriving on a later layer can never overwrite it.

Where the bug lives → if you only check visited at dequeue, two different sources can both enqueue the same cell on different layers before either is processed. You then stamp the larger (wrong) distance second. Mark on enqueue, and the problem vanishes.

REVERSE IT Seed from the destination instead of the start

“Which cells can reach the ocean / the border?” looks like it needs a BFS from every cell outward — quadratic. Flip it: make the destination the source. Seed the queue with all border cells and flood inward, reversing the movement rule (for Pacific–Atlantic, water flows downhill, so the reverse search only steps uphill). Every cell the inward flood reaches is a cell that could have flowed out.

The trick → “reachable from anyborder” is just multi-source BFS where the sources are the entire border. One sweep per ocean, then intersect.

MNEMONIC Many seeds, one frontier.

Many seeds, one frontier. Don't run one BFS per source — push all sources into the queue at distance 0 and expand once. The layer a cell is first reached on is its distance to the nearest source. Step the Visualize tab and watch the rings from every seed merge.

THE PATTERN When “nearest of many” becomes one BFS

Plain BFS from a single source S labels every cell with its distance from S, because the frontier expands one equal-distance ring at a time. Multi-source BFS exploits the fact that a frontier is just a set of cells — it works identically whether that set starts with one cell or many.

The recipe: (1) initialize the queue with every source at distance 0 and mark them all visited; (2) run the ordinary layered BFS loop; (3) the first time a cell is dequeued onto, the current layer is its distance to the nearest source. Cells never reached stay at their sentinel (-1 / ).

KEY IDEA Many seeds, layer = distance

The single insight: a BFS frontier with k seeds expands in the same equal-distance rings as a frontier with one seed. So the layer index where a cell first joins the frontier is its shortest distance to whichever source is closest — by definition, the first ring to reach it came from the nearest seed.

Layer = distance. Track distance either by storing [cell, dist]in the queue, or by processing the queue one full level at a time and incrementing a counter per level (the “minutes” variant).

COST One sweep, not k sweeps

Multi-source BFS
O(V+E)
Every cell enqueued and dequeued exactly once, total.
One BFS per source
O(k·(V+E))
k separate sweeps — the naive answer to “nearest source”.

For a grid of R×C cells, that is V = R·C and E = O(R·C) (≤ 4 edges per cell), so the whole thing is O(R·C) regardless of how many sources there are.

VARIANTS Three faces of the same trick

  • Nearest-source distance.Fill every cell with its distance to the closest source. (01-Matrix, Walls & Gates.)
  • Simultaneous spread / “minutes”. Count the number of layers until the frontier covers all required cells; return -1 if some never get reached. (Rotting Oranges.)
  • Reverse multi-source from the borders.Seed the entire border as the source set and flood inward (reversing the movement rule) to find “cells that can reach the edge.” (Pacific–Atlantic, Surrounded Regions.)

RUN IT Many seeds, one frontier — distances fill in by layer

step 0 / 8
SEEDSeed the queue with all 5 source cells (every 0) at distance 0 at once, and mark them visited now. The frontier starts as many seeds, not one.
1function nearestZero(mat: number[][]): number[][] {
2 const R = mat.length, C = mat[0].length;
3 const dist = mat.map(row => row.map(v => (v === 0 ? 0 : -1)));
4 const visited = mat.map(row => row.map(v => v === 0));
5 const dirs = [[1,0],[-1,0],[0,1],[0,-1]];
6
7 // 1. Seed the queue with ALL sources at distance 0.
8 let queue: [number, number][] = [];
9 for (let r = 0; r < R; r++)
10 for (let c = 0; c < C; c++)
11 if (mat[r][c] === 0) queue.push([r, c]);
12
13 // 2. Expand the frontier in layers.
14 while (queue.length) {
15 const next: [number, number][] = [];
16 for (const [r, c] of queue) {
17 for (const [dr, dc] of dirs) {
18 const nr = r + dr, nc = c + dc;
19 if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
20 if (visited[nr][nc]) continue; // first arrival = shortest
21 visited[nr][nc] = true; // mark on ENQUEUE
22 dist[nr][nc] = dist[r][c] + 1;
23 next.push([nr, nc]);
24 }
25 }
26 queue = next;
27 }
28 // 3. Cells still at -1 were unreachable.
29 return dist;
30}
0
0
0
0
·
0
·
·
·
source (distance 0)current cellin frontier (queue)distance settled
slowfast

TRIGGERS When you see ___ → reach for ___

Reach for multi-source BFS whenever the answer is a shortest unweighted distance to the nearest of several starting points, or a count of steps for something to spread from many origins at once. The giveaway is the word nearest, any, or simultaneously attached to a set of sources.

"nearest distance to any X" / "distance to the closest 0"multi-source BFS — seed every X at distance 0, layer = distance
"minutes until everything is reached / rots / fills"multi-source BFS counting layers; leftover unreached → return -1
"shortest distance from multiple starts"one BFS seeded with all starts, not one BFS per start
"cells reachable from any border / ocean"reverse multi-source BFS — seed the whole border, flood inward

RED FLAGSWhen it's NOT this pattern

  • Edges have weights. BFS layers only equal distance when every step costs the same. With non-negative weights you need Dijkstra (or a 0-1 BFS / deque for weights in {0,1}), not plain multi-source BFS.
  • There is exactly one source. Then it is just plain BFS — the multi-source machinery buys you nothing.
  • You need the path, not the distance, from one specific source. Multi-source BFS blends all sources together; if you must know which source or the actual route, run a single-source BFS (and store parents).

TEMPLATE Grid multi-source BFS (nearest-source distance)

When → Fill every cell with its shortest distance to the nearest source. Seed allsources at distance 0, mark them visited, then expand in layers. The first arrival at any cell is its answer. (01-Matrix, Walls & Gates.)

grid-multi-source-bfs-nearest-source-distance-.ts
// Multi-source BFS on a grid — distance to the NEAREST source.
// Classic: 01-Matrix (nearest 0), Walls & Gates (nearest gate).
function multiSourceBFS(grid: number[][]): number[][] {
  const R = grid.length, C = grid[0].length;
  const dist = grid.map(row => row.map(v => (isSource(v) ? 0 : -1)));
  const visited = grid.map(row => row.map(v => isSource(v)));
  const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];

  // 1. SEED: push EVERY source before the first expansion.
  let queue: [number, number][] = [];
  for (let r = 0; r < R; r++) {
    for (let c = 0; c < C; c++) {
      if (isSource(grid[r][c])) queue.push([r, c]);  // distance 0
    }
  }

  // 2. EXPAND in layers — drain the whole ring, then move out one step.
  while (queue.length) {
    const next: [number, number][] = [];
    for (const [r, c] of queue) {
      for (const [dr, dc] of dirs) {
        const nr = r + dr, nc = c + dc;
        if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;  // bounds
        if (visited[nr][nc]) continue;        // first arrival = shortest
        visited[nr][nc] = true;               // mark ON ENQUEUE
        dist[nr][nc] = dist[r][c] + 1;
        next.push([nr, nc]);
      }
    }
    queue = next;                             // advance to the next layer
  }

  // 3. Cells left at -1 are unreachable from any source.
  return dist;
}
Key: mark visited the instant you push, not when you pop. Distance is carried either in the queue tuple or via per-level processing.

TEMPLATE Reverse multi-source from all borders / oceans

When → “Which cells can reach the edge?” Flip the search: seed the queue with the entire border and flood inward, reversing the movement rule. Each cell the inward flood touches is one that could flow out. (Pacific–Atlantic, Surrounded Regions.)

reverse-multi-source-from-all-borders-oceans.ts
// Reverse multi-source BFS: instead of "can X reach the border?",
// seed FROM the border and flood inward. (Pacific–Atlantic, Surrounded Regions.)
function reachableFromBorder(grid: number[][]): boolean[][] {
  const R = grid.length, C = grid[0].length;
  const reach = Array.from({ length: R }, () => new Array<boolean>(C).fill(false));
  const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];

  // SEED the queue with EVERY border cell at once.
  const queue: [number, number][] = [];
  const seed = (r: number, c: number) => {
    if (!reach[r][c]) { reach[r][c] = true; queue.push([r, c]); }
  };
  for (let r = 0; r < R; r++) { seed(r, 0); seed(r, C - 1); }
  for (let c = 0; c < C; c++) { seed(0, c); seed(R - 1, c); }

  // Flood inward. For Pacific–Atlantic the climb condition reverses:
  // water flows DOWNHILL, so reverse-search only steps to HIGHER-or-equal cells.
  while (queue.length) {
    const [r, c] = queue.shift()!;
    for (const [dr, dc] of dirs) {
      const nr = r + dr, nc = c + dc;
      if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
      if (reach[nr][nc]) continue;
      if (grid[nr][nc] < grid[r][c]) continue;  // reversed flow rule
      reach[nr][nc] = true;
      queue.push([nr, nc]);
    }
  }
  return reach;  // run once per ocean, AND the two results.
}
Pacific–Atlantic: run it once per ocean (top/left vs bottom/right borders) and intersect the two reachable sets.

TEMPLATE Level-count "minutes" variant

When → You need the number of steps until the frontier covers everything, not per-cell distances. Process the queue one whole layer at a time, increment a counter per layer, and return -1 if anything is left unreached. (Rotting Oranges.)

level-count-minutes-variant.ts
// The "minutes / steps until everything is reached" variant — count LAYERS.
// Classic: Rotting Oranges (minutes until no fresh orange remains).
function minutesToFill(grid: number[][]): number {
  const R = grid.length, C = grid[0].length;
  const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];

  let queue: [number, number][] = [];
  let remaining = 0;                          // count cells still to reach
  for (let r = 0; r < R; r++) {
    for (let c = 0; c < C; c++) {
      if (grid[r][c] === 2) queue.push([r, c]);  // rotten = source
      else if (grid[r][c] === 1) remaining++;     // fresh = must be reached
    }
  }

  let minutes = 0;
  while (queue.length && remaining > 0) {
    const next: [number, number][] = [];
    for (const [r, c] of queue) {
      for (const [dr, dc] of dirs) {
        const nr = r + dr, nc = c + dc;
        if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
        if (grid[nr][nc] !== 1) continue;     // only spread to fresh cells
        grid[nr][nc] = 2;                     // mark visited by mutating
        remaining--;
        next.push([nr, nc]);
      }
    }
    queue = next;
    minutes++;                               // one whole layer = one minute
  }

  return remaining === 0 ? minutes : -1;     // leftover fresh → impossible
}
The −1 case: track how many target cells remain; if any are still unreached when the queue empties, the spread can never complete.

PITFALL Marking visited at dequeue instead of enqueue

The signature multi-source bug. If you check visited only when you pop a cell, two different sources can both enqueue the same cell on different layers before either is processed — and the farther one stamps its (larger, wrong) distance second. Mark a cell visited and stamp its distance the moment it enters the queue, so the first (nearest) source wins permanently.

PITFALL Expanding before all sources are seeded

You must push every source into the queue at distance 0 before the first expansion step. If you start BFS after seeding only some sources (or seed them lazily as you go), the layers no longer correspond to distance from the nearest source and the distances come out wrong. Seed fully, then expand.

PITFALL Forgetting unreachable cells (return −1)

Cells with no path to any source stay at their sentinel value (-1 / Infinity). In the “minutes” variant this is the difference between a valid answer and -1: after the BFS, check whether any required cell was never reached and return -1 if so (e.g. fresh oranges that can never rot).

PITFALL 4-directional vs 8-directional neighbours

The spread direction set must match the problem. Most grid problems are 4-directional ([[1,0],[-1,0],[0,1],[0,-1]]); if diagonal adjacency counts, use the 8 deltas. Using the wrong neighbour set silently produces distances that are too large (or too small).