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?”
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.
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.O(k·(V+E)) down to a single O(V+E).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).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.
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.“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.
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 / ∞).
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.
[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).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.
-1 if some never get reached. (Rotting Oranges.)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]];67 // 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 = shortest21 visited[nr][nc] = true; // mark on ENQUEUE22 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}
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 |
{0,1}), not plain multi-source BFS.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.)
// 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;
}visited the instant you push, not when you pop. Distance is carried either in the queue tuple or via per-level processing.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 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.
}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.)
// 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 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.
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.
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).
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).