994. Rotting Oranges

Rot spreads simultaneously from every infected orange at once — not one-at-a-time. The key is multi-source BFS: seed the queue with all rotten cells before the first step, then count BFS layers as minutes.

MediumMulti-source BFSMatrix BFSFlood FillTypeScript

PROBLEM What we're solving

Given an m × n grid where 0 = empty, 1 = fresh orange, 2 = rotten orange: every minute, any fresh orange 4-directionally adjacent to a rotten one becomes rotten. Return the minimum minutes until no fresh oranges remain, or -1 if impossible.

Worked example. Grid:

[[2,1,1],
 [1,1,0],
 [0,1,1]]

Minute 1: cells (0,1), (1,0) rot. Minute 2: (0,2), (1,1) rot. Minute 3: (2,1) rots. Minute 4: (2,2) rots. Answer: 4.

KEY IDEA Rot spreads simultaneously — seed BFS from all sources at once

Insight → all rotten oranges spread in parallel each minute, so a single-source BFS started from each one in sequence would over-count. Instead, enqueue every initially rotten cell before taking a single BFS step. The number of BFS layers you process equals the number of minutes elapsed. This is called multi-source BFS — the same trick used in 01 Matrix (nearest 0) and Walls and Gates.

RECIPE Multi-source BFS layer by layer

  • 0 · Seed the queue. Scan the grid once: push all 2-cells into a queue; count freshcells. This is the BFS “layer 0” — all starting sources.
  • 1 · Early exit. If fresh === 0 already, return 0 — no work needed.
  • 2 · BFS by layer. Record layerSize = queue.length - head (the number of cells added to this wave). Process exactly that many cells; for each, check all 4 neighbors. If a neighbor is fresh, mark it rotten, decrement fresh, and enqueue it. After each full layer, increment minutes.
  • 3 · Answer. Return fresh === 0 ? minutes - 1 : -1. The -1 is because the last layer that processes the final rotten cells still increments minutes once more after they infect nothing new.
Classic confusion → the loop always increments minutes after each layer — including the last one where no new cells are infected. That extra increment is why you return minutes - 1, not minutes. Many solvers get an off-by-one here. An alternative is to only increment when at least one neighbor was infected, but tracking that flag adds code.

COST Complexity & alternatives

Simulate minute by minute (copy grid each step)
O(mn · T)
T = total minutes; copies grid every round.
Multi-source BFS
O(mn)
Each cell enqueued and dequeued at most once.

Space is also O(mn) for the queue in the worst case (all cells rotten at once). The grid is mutated in-place (marking 1 → 2) so no extra visited array is needed.

Pattern transfer → multi-source BFS appears in 01 Matrix (distance to nearest 0), Walls and Gates (distance to nearest gate), Nearest Exit from Entrance in Maze, and any problem asking “minimum steps from anyof several sources to every reachable node.”

RUN IT Multi-source BFS — all rotten cells spread simultaneously

step 0 / 16
STARTFound 1 initially rotten cell and 6 fresh oranges. Enqueue all rotten cells simultaneously — this is the multi-source BFS seed.
1function orangesRotting(grid: number[][]): number {
2 const rows = grid.length;
3 const cols = grid[0].length;
4 const queue: [number, number][] = [];
5 let fresh = 0;
6
7 // Seed: enqueue ALL initially rotten cells at once (multi-source BFS)
8 for (let r = 0; r < rows; r++) {
9 for (let c = 0; c < cols; c++) {
10 if (grid[r][c] === 2) queue.push([r, c]);
11 else if (grid[r][c] === 1) fresh++;
12 }
13 }
14
15 if (fresh === 0) return 0; // nothing to rot
16
17 const dirs: [number, number][] = [[-1,0],[1,0],[0,-1],[0,1]];
18 let minutes = 0;
19 let head = 0; // pointer into queue (avoids O(n) shift)
20
21 while (head < queue.length) {
22 const layerSize = queue.length - head; // all cells in current minute
23 for (let i = 0; i < layerSize; i++) {
24 const [r, c] = queue[head++];
25 for (const [dr, dc] of dirs) {
26 const nr = r + dr;
27 const nc = c + dc;
28 if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 1) {
29 grid[nr][nc] = 2; // mark rotten in-place (visited)
30 fresh--;
31 queue.push([nr, nc]);
32 }
33 }
34 }
35 minutes++;
36 }
37
38 return fresh === 0 ? minutes - 1 : -1;
39}
💀
🍊
🍊
🍊
🍊
·
·
🍊
🍊
State
minute: 0
fresh left: 6
queue size: 1
currently spreading / activejust infected this minutealready rotten (visited)fresh (not yet infected)
slowfast

TYPESCRIPT The solution, annotated

orangesRotting.ts
function orangesRotting(grid: number[][]): number {
  const rows = grid.length;
  const cols = grid[0].length;
  const queue: [number, number][] = [];
  let fresh = 0;

  // Seed: enqueue ALL initially rotten cells at once (multi-source BFS)
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === 2) queue.push([r, c]);
      else if (grid[r][c] === 1) fresh++;
    }
  }

  if (fresh === 0) return 0;   // nothing to rot

  const dirs: [number, number][] = [[-1,0],[1,0],[0,-1],[0,1]];
  let minutes = 0;
  let head = 0;   // pointer into queue (avoids O(n) shift)

  while (head < queue.length) {
    const layerSize = queue.length - head;   // all cells in current minute
    for (let i = 0; i < layerSize; i++) {
      const [r, c] = queue[head++];
      for (const [dr, dc] of dirs) {
        const nr = r + dr;
        const nc = c + dc;
        if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 1) {
          grid[nr][nc] = 2;   // mark rotten in-place (visited)
          fresh--;
          queue.push([nr, nc]);
        }
      }
    }
    minutes++;
  }

  return fresh === 0 ? minutes - 1 : -1;
}

Reading it block by block

Lines 6–11 — seed the queue. One pass through the grid collects all rotten cells into queue and counts fresh. Crucially, every rotten cell is added before any BFS step, so they all spread simultaneously.
Line 13 — early return. If there are no fresh oranges, zero minutes pass regardless of how many rotten cells exist.
Lines 17–18 — layer loop. head is a pointer into the shared array used as a queue; advancing it is O(1) and avoids the O(n) cost of Array.shift(). layerSizecaptures how many cells belong to the current minute's wave.
Lines 19–30 — spread rot. For each cell in the current layer, check all four neighbors. A fresh neighbor is mutated to rotten (grid[nr][nc] = 2 acts as the visited marker), decrements fresh, and is appended to the queue for the next layer. After the inner loop, minutes++.
Line 33 — off-by-one fix. The last iteration of the while-loop processes cells that infect nothing new, yet still increments minutes. Return minutes - 1 to correct for this. If fresh > 0 isolated cells remain, return -1.
Complexity → O(mn) time — every cell is enqueued and dequeued at most once. O(mn) space for the queue in the worst case. The mutation of grid itself serves as the visited set, so no extra boolean matrix is needed.

INTERVIEWFollow-ups they'll ask

  • "Can you avoid mutating the input?" Use a separate boolean[][] visited grid instead of overwriting grid[r][c] = 2. Same time and space.
  • "What if rot spreads diagonally too (8-directional)?" Expand dirs from 4 to 8 entries — the BFS structure is unchanged.
  • "What if different cells have different rotting speeds?"Switch to Dijkstra's algorithm with a min-heap, using the “speed” as edge weight. Multi-source BFS becomes multi-source Dijkstra.
  • "Return the final grid state, not just the minutes?" The grid is already mutated in-place; return it alongside the minute count.
  • "What's the brute-force and why is BFS better?" A naive simulation copies the entire grid each minute and rescans it — O(mn · T). BFS processes each cell exactly once — O(mn) total.

OPTIMAL Multi-source BFS

orangesRotting.ts
function orangesRotting(grid: number[][]): number {
  const rows = grid.length;
  const cols = grid[0].length;
  const queue: [number, number][] = [];
  let fresh = 0;

  // Seed: enqueue ALL initially rotten cells at once (multi-source BFS)
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === 2) queue.push([r, c]);
      else if (grid[r][c] === 1) fresh++;
    }
  }

  if (fresh === 0) return 0;   // nothing to rot

  const dirs: [number, number][] = [[-1,0],[1,0],[0,-1],[0,1]];
  let minutes = 0;
  let head = 0;   // pointer into queue (avoids O(n) shift)

  while (head < queue.length) {
    const layerSize = queue.length - head;   // all cells in current minute
    for (let i = 0; i < layerSize; i++) {
      const [r, c] = queue[head++];
      for (const [dr, dc] of dirs) {
        const nr = r + dr;
        const nc = c + dc;
        if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 1) {
          grid[nr][nc] = 2;   // mark rotten in-place (visited)
          fresh--;
          queue.push([nr, nc]);
        }
      }
    }
    minutes++;
  }

  return fresh === 0 ? minutes - 1 : -1;
}
Complexity → O(mn) time — every cell is enqueued and dequeued at most once. O(mn) space for the queue in the worst case. The mutation of grid itself serves as the visited set, so no extra boolean matrix is needed.

ALT 1 Brute force — simulate minute by minute

O(m·n · minutes) time · O(m·n) space

Each minute, scan the whole grid, find every cell that is rotten this minute, and rot its fresh neighbors for next minute. Repeat until a full pass rots nothing.

approach-2.ts
function orangesRotting(grid: number[][]): number {
  const rows = grid.length;
  const cols = grid[0].length;
  let minutes = 0;

  for (;;) {
    // Collect cells to rot this minute first, so freshly-rotted cells
    // in THIS pass don't spread again until the next minute.
    const toRot: [number, number][] = [];
    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < cols; c++) {
        if (grid[r][c] !== 2) continue;
        for (const [dr, dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
          const nr = r + dr;
          const nc = c + dc;
          if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 1) {
            toRot.push([nr, nc]);
          }
        }
      }
    }
    if (toRot.length === 0) break;   // a stable minute: nothing more spreads
    for (const [r, c] of toRot) grid[r][c] = 2;
    minutes++;
  }

  // Any surviving fresh orange means it was unreachable.
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === 1) return -1;
    }
  }
  return minutes;
}
Note → Correct, but it re-scans every cell on every minute, so the work is O(m·n) times the number of minutes (up to m·n itself). Multi-source BFS visits each cell only once for O(m·n) total.

MNEMONIC The one-liner

"Drop all rotten oranges in the BFS pot at once — each boil is one minute."

TRIGGERS When you see ___ → reach for ___

spread / infection from multiple starting pointsmulti-source BFS
"minimum time" on a grid with simultaneous sourcesBFS layers = minutes
some cells may be unreachable → return -1check fresh count after BFS
"nearest X" from any of many cellsseed BFS queue with all X cells upfront

SKELETON The reusable shape

skeleton.ts
const queue: [number, number][] = [];
let fresh = 0;
for (let r = 0; r < rows; r++)
  for (let c = 0; c < cols; c++) {
    if (grid[r][c] === 2) queue.push([r, c]);
    else if (grid[r][c] === 1) fresh++;
  }
if (fresh === 0) return 0;
let minutes = 0, head = 0;
while (head < queue.length) {
  const layerSize = queue.length - head;
  for (let i = 0; i < layerSize; i++) {
    const [r, c] = queue[head++];
    // check 4 neighbors; if fresh, mark rotten + enqueue + fresh--
  }
  minutes++;
}
return fresh === 0 ? minutes - 1 : -1;

FLASHCARDS Tap to flip

Why must all rotten cells be enqueued before BFS starts?
They all spread simultaneously each minute; sequential BFS would over-count the time.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 9
QUESTION 01
What is the time complexity of the multi-source BFS solution?
QUESTION 02
For the grid [[2,1,1],[1,1,0],[0,1,1]], what does the algorithm return?
QUESTION 03
What is the critical difference between single-source and multi-source BFS here?
QUESTION 04
Why does the solution return minutes - 1 rather than minutes?
QUESTION 05
Grid: [[0,2]]. What does the algorithm return?
QUESTION 06
Grid: [[2,1,1],[0,1,1],[1,0,1]]. What does the algorithm return?
QUESTION 07
Why is the grid's cell value mutated to 2 rather than using a separate visited array?
QUESTION 08
#994 · Rotting OrangesMulti-source BFS seeded with all initially rotten oranges simultaneously. Count minutes as BFS layers; if any fresh orange is unreachable after BFS completes, return −1.Which algorithmic approach does this primarily use?
QUESTION 09
#994 · Rotting OrangesMulti-source BFS seeded with all initially rotten oranges simultaneously. Count minutes as BFS layers; if any fresh orange is unreachable after BFS completes, return −1.Which implementation correctly solves it?