Problem overview
The function orangesRotting(grid) receives a grid of m rows and n columns, a list of lists of integers. A cell is 0 (empty), 1 (a fresh orange) or 2 (a rotten orange). Every minute, each fresh orange directly above, below, left or right of a rotten one becomes rotten; diagonal neighbours don't count. The function returns the number of minutes until no fresh orange is left, or -1 if some orange can never rot.
Take [[2, 1, 1], [1, 1, 0], [0, 1, 1]]. The rotten orange in the corner infects the two oranges beside it in minute 1, those infect their neighbours in minute 2, and so on until minute 4, when the last orange, in the bottom right corner, rots. The answer is 4. For [[2, 1, 1], [0, 1, 1], [1, 0, 1]] the answer is -1: the orange at the bottom left has empty cells above and to its right, so rot never reaches it. For [[0, 2]] no orange is fresh from the start, so the answer is 0.
Approach: breadth-first search from every rotten orange at once
Simulating the grid minute by minute, scanning every cell each time, works but can be slow. A long winding line of oranges lets rot creep one cell per minute, so the number of minutes can grow with the number of cells, and each minute costs a full scan: up to O((m · n)²) in total. The target is O(m · n) time and space.
Rot spreading one step per minute is exactly how breadth-first search (BFS) explores a graph, level by level. Here the cells are the nodes and side-by-side cells are neighbours. The one twist is that the search starts from all the rotten oranges together, since they all spread the rot in minute 1. The oranges they rot form the second level, those oranges' neighbours the third, and so on. The number of levels that rot something is the number of minutes.
A counter of fresh oranges, lowered every time one rots, answers the second question. If it is still above zero when the queue runs out, some oranges had no path from a rotten one, and the answer is -1.
Step by step
- Read the size.
rows = len(grid)andcols = len(grid[0]). Every row has the same length, so the first row gives the number of columns. - Create an empty
dequenamedqueue. It will hold the rotten oranges that are about to spread the rot, as(row, column)tuples. Adequeremoves from the front in constant time. - Start
freshat 0. It counts the oranges that are still fresh. When it reaches 0, nothing is left to rot. - Loop
roverrange(rows)and, inside it,coverrange(cols). This first pass over the whole grid finds the starting oranges. - If
grid[r][c] == 2, append(r, c)toqueue. Every orange that is rotten at the start goes in before the search begins. - If
grid[r][c] == 1, add 1 tofresh. Empty cells are neither: nothing ever happens to them. - Create
directions. The four moves as (row change, column change):(-1, 0)up,(1, 0)down,(0, -1)left,(0, 1)right. Each changes the row or the column by one, never both, so diagonals are not neighbours. - Start
minutesat 0. If nothing is fresh at the start, no time passes. - Loop
while queue and fresh > 0. Each turn is one minute. An empty deque counts as false, soqueuealone tests that it has entries. Withoutfresh > 0the loop would count one extra minute after the last orange rots, while the last rotten oranges look for neighbours that are no longer fresh. - Add 1 to
minutesand storelevelSize = len(queue). These are the oranges that spread the rot in this minute. Oranges that rot during the minute join the same queue but must wait for the next one, so this minute handles exactlylevelSizeoranges. - Loop
levelSizetimes and take the front orange.row, col = queue.popleft()unpacks the tuple. Oranges leave in the order they rotted, so this minute's oranges come out before the ones they infect. The loop variable is_, the usual name for one that isn't used. - Loop over
directionsasdr, dcand compute the neighbour.nextRow = row + drandnextCol = col + dc. The neighbour may lie off the grid, so the next condition checks that before reading it. - If the neighbour is inside the grid and fresh, rot it. The condition
0 <= nextRow < rows and 0 <= nextCol < cols and grid[nextRow][nextCol] == 1checks the bounds first, becauseandstops at the first false part. Then setgrid[nextRow][nextCol] = 2, subtract 1 fromfreshand append(nextRow, nextCol)toqueue. Marking it rotten at once means no other orange infects it again or queues it a second time. - After the loop, return -1 if
fresh > 0. The queue ran out while some oranges were still fresh: no chain of oranges links them to a rotten one. - Otherwise return
minutes.
The solution in Python
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
rows = len(grid)
cols = len(grid[0])
queue = deque()
fresh = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
queue.append((r, c))
if grid[r][c] == 1:
fresh += 1
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
minutes = 0
while queue and fresh > 0:
minutes += 1
levelSize = len(queue)
for _ in range(levelSize):
row, col = queue.popleft()
for dr, dc in directions:
nextRow = row + dr
nextCol = col + dc
if 0 <= nextRow < rows and 0 <= nextCol < cols and grid[nextRow][nextCol] == 1:
grid[nextRow][nextCol] = 2
fresh -= 1
queue.append((nextRow, nextCol))
if fresh > 0:
return -1
return minutes
The Python details:
deque. It comes fromcollections; in a file of your own, addfrom collections import deque(andfrom typing import Listfor the hint).popleft()takes constant time, whilelist.pop(0)shifts every element and would make the search slower.- Tuple unpacking.
row, col = queue.popleft()andfor dr, dc in directionsboth split a two-element tuple into names. - Chained comparison.
0 <= nextRow < rowschecks both bounds at once. The bounds check matters because a negative index doesn't fail in Python:grid[-1]reads the last row, so the search would wrap around the grid edge and rot oranges that aren't neighbours. An index past the end raisesIndexError. - The input changes. The function marks oranges rotten inside
griditself. After a call on the first example, the grid is[[2, 2, 2], [2, 2, 0], [0, 2, 2]]. Copy the grid first if the caller needs the original.
Complexity
The first pass visits every cell once, O(m · n). In the search, each cell joins the queue at most once, because it is marked rotten when it is added, and each queued cell looks at four neighbours, so the search is O(m · n) too. The queue can hold up to O(m · n) cells, and that is the extra space.
Edge cases and common mistakes
- No fresh orange.
[[0, 2]]gives 0, and so do[[0]]and[[2, 2]]. Thewhileloop never starts, andfreshis 0. - A fresh orange and no rotten one.
[[1]]gives -1. The queue is empty from the start,freshstays 1, and the final check returns -1. - Unreachable oranges. A fresh orange walled in by empty cells or the grid's edge never rots, which is what the
freshcounter detects. Diagonal contact does not count. - Counting one minute too many. Loop on
while queuealone and the last level of rotten oranges takes one more turn that rots nothing, so the answer comes out one too big. Thefresh > 0condition prevents that. - Not processing one level at a time. Popping oranges without fixing
levelSizefirst mixes this minute's oranges with the ones they just rotted, and the minutes come out wrong. - Marking too late. Setting the neighbour to 2 only when it is popped lets it be queued several times, which also breaks the count of
fresh.
Testing
orangesRotting([[2, 1, 1], [1, 1, 0], [0, 1, 1]]) -> 4
orangesRotting([[2, 1, 1], [0, 1, 1], [1, 0, 1]]) -> -1
orangesRotting([[0, 2]]) -> 0
orangesRotting([[1]]) -> -1
orangesRotting([[2, 1, 1, 1, 1]]) -> 4
