DSA: zombie spread on a grid (multi-source BFS)
Classic multi-source BFS on a grid (a.k.a. 'rotting oranges'). Seed a queue with ALL initial zombie cells at once, then BFS level by level — each level is one time unit. Track minutes as you process levels; mark cells visited as you infect them. At the end, if any human remains, return -1. Time O(rows×cols), space O(rows×cols).
"Zombie spread on a grid" is the rotting oranges problem — the textbook multi-source BFS. Each minute, every zombie infects its 4-directional human neighbors; find the minutes until everyone is infected (or -1 if someone is unreachable).
Why multi-source BFS
All zombies spread simultaneously. If you BFS from one zombie at a time you'd get the wrong timing. Instead, seed the queue with every zombie cell up front — then a normal level-by-level BFS naturally models simultaneous spread, where each BFS level = one minute.
The algorithm
- Scan the grid once: push all zombie cells into the queue; count the humans.
- BFS level by level. For each level (minute):
- Process every cell currently in the queue.
- For each, infect 4-directional human neighbors → mark them zombie, decrement the human count, enqueue them.
- Increment
minutesafter a level that actually infected someone. - At the end: if
humans > 0, someone was unreachable → return-1. Else returnminutes.
Implementation
function timeToInfect(grid) {
const rows = grid.length, cols = grid[0].length;
const queue = [];
let humans = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] === 2) queue.push([r, c]); // 2 = zombie
else if (grid[r][c] === 1) humans++; // 1 = human
}
}
const dirs = [[1,0],[-1,0],[0,1],[0,-1]];
let minutes = 0;
while (queue.length && humans > 0) {
const size = queue.length; // freeze this level's size
for (let i = 0; i < size; i++) {
const [r, c] = queue.shift();
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 1) {
grid[nr][nc] = 2; // infect — also serves as 'visited'
humans--;
queue.push([nr, nc]);
}
}
}
minutes++; // one full level = one minute
}
return humans === 0 ? minutes : -1;
}Complexity
- Time: O(rows × cols) — every cell is enqueued and visited at most once.
- Space: O(rows × cols) — worst case the queue holds all cells.
Key details interviewers probe
- Why all sources at once — the crux. Single-source BFS gives wrong minute counts.
- Freeze the level size (
const size = queue.length) before the inner loop — otherwise you'd merge levels. - Mark visited by mutating the grid (
grid[nr][nc] = 2) — no separate visited set needed; prevents double-counting. - The -1 case — humans walled off from any zombie.
- Edge cases — no zombies but humans exist → -1; no humans at all → 0.
- BFS not DFS — BFS gives shortest/level distance; DFS doesn't model simultaneous time steps.
Senior framing
The senior signal is naming the pattern immediately — "this is multi-source BFS, the rotting-oranges template" — and articulating why seeding all sources models simultaneous spread, plus the level-size-freeze detail. It generalizes: shortest distance from multiple starting points, fire spread, flood fill timing — all the same template.
Follow-up questions
- •Why must you seed the queue with all zombies before starting BFS?
- •How do you detect the unreachable-human (-1) case?
- •How would this change if zombies could move diagonally?
- •Why BFS and not DFS here?
Common mistakes
- •Doing single-source BFS from each zombie separately — wrong timing.
- •Not freezing queue.length before the level loop, merging minutes.
- •Forgetting the -1 case for unreachable cells.
- •Using a separate visited set instead of mutating the grid (works, just unnecessary).
Edge cases
- •No humans at all → 0 minutes.
- •Humans present but no zombies → -1.
- •Humans completely walled off → -1.
- •Empty grid.
Real-world examples
- •Rotting oranges (LeetCode 994), fire/flood spread simulations, shortest distance from multiple sources.