Problem overview
The method OrangesRotting receives a grid as a jagged array, int[][] grid. Each cell is 0 (empty), 1 (a fresh orange) or 2 (a rotten orange). Every minute, each fresh orange that touches a rotten one above, below, left or right becomes rotten. Diagonal contact doesn't count. The method returns the number of minutes until no fresh orange is left, or -1 if some orange can never rot.
In the first example, the grid {{2,1,1},{1,1,0},{0,1,1}} takes 4 minutes: the rot travels outward from the corner one step per minute, and the bottom-right orange is the last to turn. In the second example, {{2,1,1},{0,1,1},{1,0,1}}, the orange in the bottom-left corner has empty cells above and to its right, so nothing reaches it, and the answer is -1. For {{0,2}} there is no fresh orange, so the answer is 0.
The lesson asks for O(m · n) time and space for an m by n grid.
Approach: spread the rot level by level
The direct simulation scans the whole grid every minute. On a long winding line of oranges that the rot creeps along one cell per minute, the number of minutes is proportional to the number of cells, so the total is quadratic in the grid size.
A breadth-first search (BFS) is a better fit, because breadth-first order is exactly the order in which the rot spreads. Start with all the rotten oranges in a queue at once, since they all begin spreading in minute 1. The oranges that rot in minute 1 form the first level, the oranges they rot form the second level, and so on. The number of levels processed is the number of minutes.
Two more pieces finish the job:
- A counter
freshholds the number of fresh oranges and goes down each time one rots. If it is still above 0 when the queue runs out, no chain of oranges connects those oranges to a rotten one, and the answer is -1. - Rotting an orange immediately, by writing 2 into its cell, makes sure it is queued only once. Each cell enters the queue at most once and looks at four neighbours, which gives
O(m · n)work.
The loop condition also includes fresh > 0. Without it, the loop would count one more minute after the last orange rots, while the final rotten oranges look for fresh neighbours that no longer exist. On the first example, that version returns 5 instead of 4, and on {{2,1}} it returns 2 instead of 1.
Step by step
- Store the grid's size.
rowsisgrid.Lengthandcolsisgrid[0].Length, the length of the first row, since every row has the same length. - Create the queue, a
Queue<(int, int)>of (row, column) tuples. It holds the rotten oranges that are about to spread the rot. - Create
fresh, starting at 0. - Scan every cell with two nested loops,
rover rows andcover columns. A cell equal to 2 is enqueued as(r, c), and a cell equal to 1 adds one tofresh. Empty cells are neither. - Describe the four moves in
directions, an array of (row change, column change) tuples: up, down, left and right. Each changes only the row or only the column, so diagonal neighbours are excluded. - Create
minutes, starting at 0. With no fresh orange at the start, 0 is the answer. - Loop while the queue isn't empty and
freshis above 0. Each turn of the loop is one minute, so add one tominutesat the top. - Record
levelSize, the queue's current count. Those oranges spread the rot this minute. Oranges that rot during the minute join the same queue, but they act in the next minute, so the inner loop runs exactlylevelSizetimes. - Dequeue one orange and unpack the tuple into
rowandcol. - Visit the four neighbours with a
foreachoverdirections, unpacking each move intodranddc, and computenextRowandnextCol. - Check the neighbour: inside the grid and fresh. The bounds come first, and
&&stops at the first false part, so the grid is only read for cells that exist. - Rot it: set the cell to 2, subtract one from
freshand enqueue(nextRow, nextCol). - After the loop, return -1 if
freshis still above 0, andminutesotherwise.
The solution in C#
class Solution {
public int OrangesRotting(int[][] grid) {
int rows = grid.Length;
int cols = grid[0].Length;
Queue<(int, int)> queue = new Queue<(int, int)>();
int fresh = 0;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] == 2) {
queue.Enqueue((r, c));
}
if (grid[r][c] == 1) {
fresh++;
}
}
}
(int, int)[] directions = { (-1, 0), (1, 0), (0, -1), (0, 1) };
int minutes = 0;
while (queue.Count > 0 && fresh > 0) {
minutes++;
int levelSize = queue.Count;
for (int i = 0; i < levelSize; i++) {
(int row, int col) = queue.Dequeue();
foreach ((int dr, int dc) in directions) {
int nextRow = row + dr;
int nextCol = col + dc;
if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols && grid[nextRow][nextCol] == 1) {
grid[nextRow][nextCol] = 2;
fresh--;
queue.Enqueue((nextRow, nextCol));
}
}
}
}
if (fresh > 0) {
return -1;
}
return minutes;
}
}
The C# details worth knowing:
- Jagged arrays.
int[][]is an array of arrays, indexedgrid[r][c].grid.Lengthcounts the rows andgrid[0].Lengthcounts the columns. This differs from the rectangularint[,], which usesgrid[r, c]. - Tuples.
(int, int)is a value tuple: two integers kept together.queue.Enqueue((r, c))needs the double parentheses, one pair for the call and one for the tuple.(int row, int col) = queue.Dequeue();deconstructs a tuple into two new variables. - Array initializer.
(int, int)[] directions = { ... };builds the array of tuples with nonew, because the declared type gives the element type. foreachwith deconstruction.foreach ((int dr, int dc) in directions)unpacks every tuple as the loop goes. It is why the loop body can usedranddcdirectly.Queue<T>. It lives inSystem.Collections.Generic.Enqueueadds at the back,Dequeueremoves from the front, andCountis a property. Dequeuing from an empty queue throws, but the loop condition prevents that.- The input is changed. The method writes 2 into the caller's grid, so every orange that could rot is rotten afterwards. Copy the grid first if the original is needed again.
- Reading
queue.Countonce.levelSizeis stored before the inner loop because the count changes as oranges are enqueued. Usingqueue.Countas the loop bound would mix levels together.
Complexity
Time: O(m · n). The first scan visits each cell once. In the search, each cell is enqueued at most once, since it turns rotten the moment it is enqueued, and each dequeue checks four neighbours with constant work. A 300 by 300 grid of fresh oranges with one rotten corner takes 598 minutes and runs instantly.
Space: O(m · n). In the worst case the queue holds a large share of the cells, for example when most of the grid starts rotten.
Edge cases and common mistakes
- No fresh oranges.
{{0,2}}and{{0}}both return 0. The loop never starts, becausefreshis 0. - No rotten oranges but a fresh one.
{{1}}returns -1: the queue is empty, so nothing can ever rot. - Several rotten sources.
{{2,1,1,2}}returns 1, because both rotten oranges spread in the same minute. Starting from one source at a time would give a wrong, larger answer. - A blocked orange.
{{2,0,1}}returns -1. An empty cell stops the spread, and the orange beyond it is never reached. - Counting the extra minute. Dropping
fresh > 0from the loop condition overcounts by one whenever every orange rots. - Diagonal spread. Adding diagonal moves to
directionswould rot oranges that the problem says stay fresh, such as the corner in the second example. - Not marking oranges when they are enqueued. If the cell is only marked when it is dequeued, the same orange can be queued several times, and
freshis decremented more than once for it. - Checking the grid before the bounds. Reading
grid[nextRow][nextCol]first throws anIndexOutOfRangeExceptionat the edges. Keep the bounds checks on the left of the&&chain.
Testing
{{2,1,1},{1,1,0},{0,1,1}}gives4.{{2,1,1},{0,1,1},{1,0,1}}gives-1.{{0,2}}gives0.{{1}}gives-1.{{2,1,1,1,1}}gives4.{{2,1,1,2}}gives1.{{2,0,1}}gives-1.
