Algorithm

Rotting Oranges

Breadth-First Search (BFS) Pattern

Rotting Oranges

You are given an m x n grid where each cell holds 0 for an empty cell, 1 for a fresh orange, or 2 for a rotten orange.

Each minute, every fresh orange that is 4-directionally adjacent to a rotten orange becomes rotten. All such changes happen simultaneously within the same minute. Return the number of minutes that elapse until no fresh orange remains, or -1 if some fresh orange can never rot.

If there are no fresh oranges at the start, the answer is 0, however many rotten oranges are present.

CONSTRAINTS
  • m == grid.length, n == grid[i].length
  • 1 <= m, n <= 10
  • grid[i][j] is 0, 1, or 2
  • Rotting spreads only up, down, left and right — never diagonally.
  • Empty cells block the spread; rot cannot cross them.
EXAMPLE 1
Input: grid = [[2,1,1],[1,1,0],[0,1,1]]
Output: 4
The last orange to go is (2,2). Rot leaves the single rotten cell at (0,0) and must travel around the empty cell at (1,2), taking one minute per adjacency step. The answer is the time the slowest orange takes, not the total number of oranges that rot.
EXAMPLE 2
Input: grid = [[2,1,1],[0,1,1],[1,0,1]]
Output: -1
The fresh orange at (2,0) has an empty cell above it and an empty cell beside it, so no rotten orange is ever 4-directionally adjacent to it. It stays fresh forever, so the answer is -1 no matter how quickly the others rot.
EXAMPLE 3
Input: grid = [[0,2]]
Output: 0
There are no fresh oranges to begin with, so the condition "no cell has a fresh orange" already holds at minute 0. No time passes.
EXAMPLE 4
Input: grid = [[1]]
Output: -1
A fresh orange exists but there is no rotten orange anywhere to start the spread. With no source, nothing ever happens, so it is impossible.
If several oranges are rotten at the start, do they spread at the same time or one after another?
All at the same time. Every rotten orange infects its neighbours during the same minute. This matters enormously — treating them sequentially gives wrong answers whenever two rot fronts converge on the same region.
Does rot spread diagonally?
No, only up, down, left and right. That restriction is what makes some grids impossible — an orange diagonally adjacent to rot is not adjacent for this problem's purposes.
What should I return if there are no oranges at all, or no fresh ones?
0. The requirement is that no fresh orange remains, and an empty or fully-rotten grid already satisfies it. Returning -1 here is a common wrong answer.
Am I returning the time, or the number of oranges that rotted?
The time, measured in minutes, which equals the time the last orange takes to rot. Many oranges may rot within the same minute and that counts once.

Read the rule carefully: every fresh orange next to a rotten one turns rotten within the same minute, and all of this happens at once. So the process is not a chain of individual events — it is a whole layer of the grid flipping state, then the next layer, then the next. The question asks how many layers it takes to consume everything.

That should immediately suggest a picture of fronts advancing in step. The job is to make that picture into an algorithm without accidentally letting one front run ahead of another.

The literal simulation, and where it wastes work

The most direct translation of the rules: loop once per minute, and on each pass scan the entire grid, marking every fresh orange that currently touches rot.

python
def oranges_rotting(grid):
    minutes = 0
    while True:
        newly_rotten = []
        for r in range(len(grid)):
            for c in range(len(grid[0])):
                if grid[r][c] == 1 and touches_rot(grid, r, c):
                    newly_rotten.append((r, c))
        if not newly_rotten:
            break
        for r, c in newly_rotten:
            grid[r][c] = 2          # apply all at once, AFTER the scan
        minutes += 1
    return minutes if no_fresh_left(grid) else -1

This is correct, and the two-phase structure is not optional: if you flipped cells to 2 during the scan itself, a cell rotted this minute could infect its neighbour in the very same pass, and rot would race across the grid far too fast. Collecting first and applying afterwards is what enforces simultaneity.

Correct, but wasteful, and the waste has a precise shape. Each minute rescans all m × n cells, but almost all of them are irrelevant — empty cells, long-rotten oranges, and fresh oranges nowhere near the action. Only the cells adjacent to those that rotted last minute can possibly change. With up to m × n minutes and a full scan each time, the cost is O((m × n)²).

Name the waste exactly: we keep searching the grid for the frontier, when we could have simply remembered it.

The reframe: carry the frontier forward

Keep a list of the oranges that rotted during the previous minute. Those are the only cells capable of infecting anything now. Process exactly those, collect whatever they infect, and that collection becomes the next minute's frontier. The grid is never scanned again after the initial setup.

Two consequences fall out. First, each cell is touched a constant number of times overall, so the whole simulation is linear in the grid size. Second, and more interesting: the number of frontier generations is the answer. Time is not something you track separately alongside the spread — it is the depth of the spread.

The structure holding the frontier is a queue: add at the back, remove from the front. That FIFO discipline is what keeps generation d fully processed before any of generation d+1 begins. A stack would let one branch race ahead and the minute count would be meaningless. This ring-by-ring expansion is Breadth-First Search, and the version here has a particular twist worth naming.

Ordinary BFS starts from one cell. Here, every initially rotten orange starts spreading at the same instant, so every one of them is seeded into the queue before the loop begins. That is multi-source BFS, and the correctness argument is worth being able to state: with all sources at distance 0, the wave that first reaches any cell came from whichever source is nearest to it, so each cell is claimed at exactly the minute it would truly rot. You get the effect of running a separate search from every rotten orange, at the cost of one.

The mechanism

The impossibility case needs handling too, and there is a neat way to do it: count the fresh oranges during setup and decrement as each one rots. If the counter is not zero when the queue empties, the survivors were unreachable.

python
from collections import deque

def oranges_rotting(grid):
    rows, cols = len(grid), len(grid[0])
    queue = deque()
    fresh = 0

    for r in range(rows):                     # setup: seed every source at once
        for c in range(cols):
            if grid[r][c] == 2:
                queue.append((r, c))
            elif grid[r][c] == 1:
                fresh += 1

    minutes = 0
    while queue and fresh > 0:
        minutes += 1
        for _ in range(len(queue)):           # freeze the size: exactly this minute's front
            r, c = queue.popleft()
            for nr, nc in ((r-1,c), (r+1,c), (r,c-1), (r,c+1)):
                if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
                    grid[nr][nc] = 2
                    fresh -= 1
                    queue.append((nr, nc))

    return minutes if fresh == 0 else -1
Crucial Notefor _ in range(len(queue)) reads the queue's length once, before the loop body starts appending to it. That frozen count is exactly the set of oranges that rotted last minute. If you wrote a plain while queue here, this minute's newly infected oranges would be processed inside this same minute, the generations would smear together, and the count would come out as roughly the number of oranges rather than the number of minutes. Freezing the size is what draws the line between one minute and the next.

The second subtlety is the fresh > 0 condition on the outer loop. Without it, the final generation — the one that infects nothing, because everything is already rotten — would still increment minutes, giving an answer one too large. Stopping the moment the last fresh orange is consumed also handles the no-fresh-oranges grid correctly: the loop never runs and 0 is returned.

Setup visits every cell once, and each orange enters and leaves the queue at most once, so the whole thing is O(m × n) time and O(m × n) memory for the queue.

Worked example:grid = [[2,1,1],[1,1,0],[0,1,1]]

Setup finds one rotten orange at (0,0), so the queue starts as [(0,0)], and counts 7 fresh oranges — every cell except (0,0), (1,2) and (2,0).

- Minute 1. Frozen size 1. Pop (0,0). Its neighbours (0,1) and (1,0) are fresh, so both rot. Fresh drops to 5. Queue: [(0,1), (1,0)].
- Minute 2. Frozen size 2, so exactly these two are processed and nothing discovered now is touched. (0,1) infects (0,2) and (1,1). (1,0) finds (1,1) already rotten this same minute, and (2,0) is an empty cell that blocks the spread. Fresh drops to 3. Queue: [(0,2), (1,1)].
- Minute 3. (0,2) has (1,2) below it, which is empty — no spread that way. (1,1) infects (2,1). Fresh drops to 2. Wait: (1,1)'s neighbours are (0,1) rotten, (1,0) rotten, (1,2) empty, and (2,1) fresh. So only (2,1) rots. Fresh drops to 2. Queue: [(2,1)].
- Minute 4. (2,1) infects (2,2). Fresh drops to 1... and then to 0 — recount: after minute 3 the remaining fresh oranges were (2,2) alone, since 7 minus 2 minus 2 minus 1 is 2, and those two were (2,1) and (2,2), with (2,1) rotting in minute 3. So after minute 3 only (2,2) is fresh, and minute 4 rots it. Fresh reaches 0.

The loop condition now fails and the answer is 4. Notice the rot had to travel around the empty cell at (1,2) rather than through it — the empty cell is why the far corner takes 4 minutes rather than 3.

For [[2,1,1],[0,1,1],[1,0,1]], the spread proceeds normally but (2,0) has an empty cell above at (1,0) and an empty cell beside at (2,1). It is never adjacent to anything rotten, so when the queue drains, fresh is still 1 and the answer is -1.

The reusable idea

Two things transfer out of this problem.

The first is multi-source BFS. Whenever many starting points act simultaneously — infections, fires spreading, signals broadcast from several towers, water rising from every coastline — seed all of them into the queue before the loop. You are not running many searches; you are running one search whose distance-0 layer happens to have many members. It costs the same as a single-source search and answers "distance to the nearest source" for every cell in the grid at once.

The second is the identification of time with layer depth. The moment a problem's rules say "each step, everything adjacent changes", the number of steps is the number of BFS generations, and the frozen-size loop is how you count them. That pattern recurs directly in problems about infection times in trees and about how long it takes a signal to reach every node in a network.

Train the reflex: when a statement says many things spread at once and asks how long until everything is covered, do not simulate one spreader at a time and do not rescan the world each tick. Seed every source into one queue and count generations.

Interactive Strategy Visualization
Phase 1 — Scanning
Multi-Source BFS Infection
🤢(0,0)
🍊(0,1)
(0,2)
🍊(1,0)
🍊(1,1)
🍊(1,2)
(2,0)
🍊(2,1)
🍊(2,2)
0 MIN
Rotten
Fresh
In Queue
Active Source
Infection Queue (0)empty

Start: Scanning grid for initial rotten sources and counting fresh oranges.

STEP 1/26
O((M × N)²) Rescan Grid Each Minute
O(M × N) Multi-Source BFS
O(M × N) Single Pass With Fresh Counter