Rotting Oranges (LC 994)
On this page
- Problem Statement
- 1. Algorithm & Pseudocode
- 2. Step-by-Step Analysis (Beginner-Friendly)
- 3. The Dry Run
- 4. Java Solution
- Brute Force (BFS per source — correct but wasteful)
- Optimal (Multi-Source BFS)
- 5. The “Java vs. Others” Edge
- 6. Complexity Summary
- 7. Edge Cases & Follow-Ups
- Related Problems (the multi-source BFS family)
Pattern: Queue — Multi-Source BFS
Difficulty: Medium
Key Concept: Everything rots simultaneously, so seed the queue with all rotten cells before the loop starts. BFS levels then equal elapsed minutes.
Problem Statement
You are given an m × n grid where each cell is:
0— empty1— a fresh orange2— a rotten orange
Every minute, any fresh orange 4-directionally adjacent to a rotten one becomes rotten.
Return the minimum number of minutes until no fresh orange remains, or -1 if that’s impossible.
Input: int[][] grid, 1 <= m, n <= 10
Output: int — minutes, or -1
Example
[[2,1,1], minute 0 minute 1 minute 2 minute 3 minute 4
[1,1,0], 2 1 1 2 2 1 2 2 2 2 2 2 2 2 2
[0,1,1]] 1 1 0 → 2 1 0 → 2 2 0 → 2 2 0 → 2 2 0
0 1 1 0 1 1 0 1 1 0 2 1 0 2 2
Output: 4
1. Algorithm & Pseudocode
Wrong instinct: run BFS from each rotten orange separately
for each rotten cell: BFS the whole grid, record distances, take min per fresh cell
Correct but O(R · m · n) — and it misses the point: the sources spread at the same time.
Optimal (multi-source BFS)
queue = empty
fresh = 0
// PASS 1 — seed the queue with EVERY rotten cell, and count the fresh ones
for each cell (r,c):
if grid[r][c] == 2: queue.offer((r,c))
if grid[r][c] == 1: fresh++
if fresh == 0: return 0 // nothing to rot — answer is 0, not -1
minutes = 0
while queue not empty AND fresh > 0:
size = queue.size() // ← FREEZE: this is one full minute
for i in 1..size:
(r,c) = queue.poll()
for each of the 4 neighbours (nr,nc):
if in bounds AND grid[nr][nc] == 1:
grid[nr][nc] = 2 // mark rotten WHEN ENQUEUED, not when polled
fresh--
queue.offer((nr,nc))
minutes++
return fresh == 0 ? minutes : -1 // leftover fresh = unreachable
2. Step-by-Step Analysis (Beginner-Friendly)
-
Recognising it as BFS at all “Minimum number of minutes” over an unweighted grid = shortest path = BFS. Each minute is exactly one BFS level. DFS would give you a path, not the shortest one.
-
Why multi-source, and why it’s free All rotten oranges spread in parallel. Instead of running BFS
Rtimes and taking minimums, put allRsources in the queue at level 0. BFS then naturally expands the closest source to each cell first — you get the min-over-sources for free, in a single O(m·n) pass. This is the single most reusable queue trick in interviews. -
int size = q.size()is what makes levels work Freezing the queue size before the inner loop means you process exactly the cells that rotted in the previous minute, and the ones you enqueue belong to the next minute. Drop this line and the levels smear together and your minute count is wrong. -
Mark rotten on ENQUEUE, never on dequeue If you only set
grid[nr][nc] = 2when you poll it, the same cell can be enqueued by several neighbours in the same minute. You’d double-countfresh, blow up the queue, and still be counting the same orange twice. Mark at enqueue time — this is the general BFS rule (“mark visited when you add, not when you remove”). -
The
freshcounter answers both questions It tells you when to stop early, and at the endfresh > 0means those oranges were walled off by empty cells and can never rot → return-1. You don’t need a second grid sweep. -
The
fresh == 0early return matters A grid with no fresh oranges at all is already done: the answer is 0, not-1. This is the single most common wrong submission on this problem. -
Why
minutesisn’t off by one The loop condition includesfresh > 0, so the final iteration — the one that would run after the last orange rots and enqueue nothing — never executes. Without that guard you’d returnminutes + 1.
3. The Dry Run
grid = [[2,1,1],[1,1,0],[0,1,1]]
Seed: queue = [(0,0)], fresh = 6
| Minute | Queue at start (size) | Newly rotted | fresh after |
minutes |
|---|---|---|---|---|
| 1 | [(0,0)] (1) |
(0,1), (1,0) | 4 | 1 |
| 2 | [(0,1),(1,0)] (2) |
(0,2), (1,1) | 2 | 2 |
| 3 | [(0,2),(1,1)] (2) |
(2,1) | 1 | 3 |
| 4 | [(2,1)] (1) |
(2,2) | 0 | 4 |
fresh == 0 → loop exits → return 4 ✓
Grid state per minute:
min 0 min 1 min 2 min 3 min 4
2 1 1 2 2 1 2 2 2 2 2 2 2 2 2
1 1 0 2 1 0 2 2 0 2 2 0 2 2 0
0 1 1 0 1 1 0 1 1 0 2 1 0 2 2
4. Java Solution
Brute Force (BFS per source — correct but wasteful)
// For every rotten cell, BFS the grid recording distance to each fresh cell,
// keep the minimum distance per fresh cell, then take the max over all fresh cells.
// Time O(R * m * n) where R = number of rotten oranges. Skipped in full —
// the point is that multi-source BFS collapses all R passes into one.
Optimal (Multi-Source BFS)
class Solution {
private static final int[][] DIRS = {{1,0}, {-1,0}, {0,1}, {0,-1}};
public int orangesRotting(int[][] grid) {
int rows = grid.length, cols = grid[0].length;
Queue<int[]> queue = new ArrayDeque<>();
int fresh = 0;
// PASS 1: seed every rotten cell + count fresh
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] == 2) queue.offer(new int[]{r, c});
else if (grid[r][c] == 1) fresh++;
}
}
if (fresh == 0) return 0; // nothing to rot → 0 minutes, NOT -1
int minutes = 0;
while (!queue.isEmpty() && fresh > 0) {
int size = queue.size(); // freeze: everything that rotted last minute
for (int i = 0; i < size; i++) {
int[] cell = queue.poll();
for (int[] d : DIRS) {
int nr = cell[0] + d[0], nc = cell[1] + d[1];
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
if (grid[nr][nc] != 1) continue; // empty or already rotten
grid[nr][nc] = 2; // mark WHEN ENQUEUED — prevents double-counting
fresh--;
queue.offer(new int[]{nr, nc});
}
}
minutes++; // one full level = one minute
}
return fresh == 0 ? minutes : -1; // leftovers are unreachable
}
}
Time O(m·n) — every cell enqueued at most once · Space O(m·n) for the queue
5. The “Java vs. Others” Edge
ArrayDequeas the BFS queue, neverLinkedList. Both implementQueue, butArrayDequeis a circular array — no per-element node allocation, far better cache locality.LinkedListis what old tutorials show;ArrayDequeis what the JDK docs recommend.int[]as the coordinate, not aPointclass or aStringkey.new int[]{r, c}is one small allocation with no boxing. Encoding as"r,c"strings means hashing and parsing on every access — a real slowdown and a common junior habit. An even tighter option: encode as a singleint(r * cols + c) and decode with/and%.static final int[][] DIRS— hoisted out of the method so the array isn’t reallocated on every call, and it names the intent. This is the standard Java idiom for grid neighbours.- Mutating the input grid as the visited set — legal here and O(1) space beyond the queue. In production you’d copy first; in an interview, say “I’m mutating the input as my visited marker; I’d defensively copy if the caller needs it preserved.”
offer/pollreturn sentinels,add/removethrow. Prefer the sentinel pair with anisEmpty()guard — no exception handling in the hot loop.
6. Complexity Summary
| Approach | Time | Space | Notes |
|---|---|---|---|
| BFS per rotten source | O(R·m·n) | O(m·n) | Recomputes the same distances R times |
| Multi-source BFS | O(m·n) | O(m·n) | Single pass; each cell enqueued once |
| DFS | ✗ | — | Gives a time, not the minimum — wrong tool |
7. Edge Cases & Follow-Ups
| Case | Expected | Why it trips people |
|---|---|---|
[[0]] |
0 |
No fresh oranges → done at minute 0, not -1 |
[[2,2]] |
0 |
All rotten already |
[[1]] |
-1 |
Fresh with no source ever |
[[2,1,1],[0,1,1],[1,0,1]] |
-1 |
Bottom-left 1 is walled off by 0s |
[[1,2]] |
1 |
Single-step spread |
Follow-ups to expect
- 8-directional spread? → add the 4 diagonals to
DIRS. Nothing else changes. - Different oranges rot at different rates? → weighted edges, so BFS no longer works;
switch to Dijkstra with a
PriorityQueue. - Which orange rotted last? → track the cell popped on the final level.
- Huge sparse grid? → the queue only ever holds the frontier, so memory stays O(frontier).
Related Problems (the multi-source BFS family)
| # | Problem | Difficulty | Sources seeded |
|---|---|---|---|
| LC 542 | 01 Matrix | Medium | every 0 cell |
| LC 286 | Walls and Gates | Medium | every gate |
| LC 1162 | As Far from Land as Possible | Medium | every land cell |
| LC 417 | Pacific Atlantic Water Flow | Medium | every ocean-edge cell (two BFS runs) |
| LC 200 | Number of Islands | Medium | single-source BFS/DFS per island |
| LC 127 | Word Ladder | Hard | BFS on an implicit graph |