Start learning free

LESSON 479 OF 543 · GRAPHS

Rotting Oranges in Rust: O(m · n) Multi-Source BFS

Practice O(m · n) Read the full explanation

You are given a grid grid with m rows and n columns: an array of rows, where each row is an array of integers. Rows are numbered from 0, top to bottom, and columns from 0, left to right. Every cell holds one of three values:

  • 0: an empty cell,
  • 1: a fresh orange,
  • 2: a rotten orange.

Every minute, each fresh orange that is next to a rotten orange (up, down, left or right, not diagonally) becomes rotten.

Return the number of minutes that pass until no cell holds a fresh orange. If some orange can never rot, return -1.

 

Example 1:

Input: grid = [
  [2, 1, 1],
  [1, 1, 0],
  [0, 1, 1]
]
Output: 4
Explanation: The grid after each minute:
  start      minute 1   minute 2   minute 3   minute 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
In minute 1 the two oranges next to the rotten one in the corner rot; in each later minute the
oranges next to those rot in turn. After minute 4 no orange is fresh.

Example 2:

Input: grid = [
  [2, 1, 1],
  [0, 1, 1],
  [1, 0, 1]
]
Output: -1
Explanation: The orange in the bottom left corner, [2, 0], has an empty cell above it and to
its right. Rot doesn't spread diagonally, so nothing ever reaches it: it stays fresh forever.

Example 3:

Input: grid = [[0, 2]]
Output: 0
Explanation: No orange is fresh at the start, so no minute has to pass.

 

Constraints:

  • 1 <= m, n <= 300
  • Every row has n values.
  • Every grid[r][c] is 0, 1 or 2.

Note:

Your algorithm should run in O(m · n) time and use O(m · n) extra space.

Simulating the grid minute by minute, with a scan of every cell in each minute, can cost O((m · n)2): think of a long winding line of oranges that the rot creeps along one cell per minute. The tutorial uses a BFS that starts from all the rotten oranges at once, as the BFS in 01 Matrix starts from all the zeros, and goes through it level by level: the oranges that rot in minute 1 are the first level, the oranges they rot are the second level, and so on, so the number of levels is the number of minutes. A count of the fresh oranges, lowered each time one rots, tells at the end whether some orange never rotted. Each cell joins the queue at most once.

STEP 1 OF 25
    Ctrl + Enter to check

    RUST · GRAPHS

    Rotting Oranges in Rust, explained

    Problem overview

    The function oranges_rotting receives a grid of m rows and n columns, a Vec<Vec<i32>>. Each cell is 0 (empty), 1 (a fresh orange) or 2 (a rotten orange). Every minute, each fresh orange that is directly next to a rotten one (up, down, left or right, not diagonally) becomes rotten. The function returns the number of minutes until no fresh orange is left, or -1 if some orange can never rot.

    For the grid [[2, 1, 1], [1, 1, 0], [0, 1, 1]] the answer is 4. The rot starts in the top-left corner and creeps outward one step per minute until it reaches the bottom-right corner. For [[2, 1, 1], [0, 1, 1], [1, 0, 1]] the answer is -1: the orange in the bottom-left corner has empty cells above it and to its right, so the rot never reaches it. For [[0, 2]] the answer is 0, because nothing is fresh at the start.

    Approach: breadth-first search from all rotten oranges at once

    A direct simulation scans the whole grid every minute, looking for fresh oranges next to rotten ones. On a long winding line of oranges the rot advances one cell per minute, so the number of minutes can be proportional to the number of cells, and the total work grows like (m · n)².

    A breadth-first search (BFS) fits better. Rot spreads outward from its sources one step at a time, and BFS explores a graph the same way: first everything one step from the start, then everything two steps away, and so on. The twist is that there are several sources, every orange that is rotten at minute 0, so they all go into the queue before the search starts. The oranges that rot in minute 1 form the first level, the ones they rot form the second level, and so on. The number of levels is the number of minutes.

    A counter of fresh oranges, lowered each time one rots, answers the -1 question. When the queue runs out and the counter is still above 0, those oranges are cut off from every rotten one.

    Each cell enters the queue at most once, because it is marked rotten the moment it is added, so the work is proportional to the number of cells.

    For the first example, after each minute the code has this many oranges in the queue (those that will spread the rot next) and this many fresh oranges left:

    minute 1  queue_len=2  fresh=4
    minute 2  queue_len=2  fresh=2
    minute 3  queue_len=1  fresh=1
    minute 4  queue_len=1  fresh=0
    

    After minute 4 the loop stops with fresh = 0, and the answer is 4.

    Step by step

    1. Make the grid mutable with let mut grid = grid;. The search marks the oranges it rots, and the new binding takes over the vector the function received.
    2. Store the dimensions: rows is grid.len(), and cols is grid[0].len(), since every row has the same length.
    3. Create the queue, a VecDeque<(usize, usize)>. It holds the rotten oranges that are about to spread the rot, as (row, column) tuples. push_back and pop_front both run in constant time.
    4. Create fresh at 0, the count of fresh oranges.
    5. Scan the grid with r over 0..rows and, inside it, c over 0..cols. This first pass finds the starting sources and counts the fresh oranges.
    6. Queue every rotten orange: if grid[r][c] == 2, push (r, c). All of them spread the rot in the first minute, so they all start in the queue.
    7. Count every fresh orange: if grid[r][c] == 1, add 1 to fresh. Empty cells are neither.
    8. Define the four directions as an array of (i32, i32) tuples: up, down, left and right. Each move changes the row or the column by one, never both, so diagonals aren't neighbours. The moves are i32 because they can be negative.
    9. Start minutes at 0. If no orange is fresh, no time passes.
    10. Loop while the queue is not empty and fresh > 0. Each turn is one minute. Without the fresh > 0 test, the loop would count one extra minute after the last orange rots.
    11. Add 1 to minutes. A new minute starts.
    12. Record the queue length in level_size. These are the oranges that spread the rot this minute. Oranges that rot during the minute join the same queue but spread the rot only in the next one.
    13. Repeat level_size times, with _ as the unused loop variable. One pass of this loop handles one level of the BFS.
    14. Pop the front tuple and unpack it into (row, col).
    15. Loop over the directions with the pattern &(dr, dc).
    16. Compute the neighbour as i32 values: row as i32 + dr and col as i32 + dc.
    17. Check that the neighbour is inside the grid. In i32, a step off the top or left edge is -1, which the check sees; as a usize it would not exist.
    18. Convert back to usize by shadowing the two names, now safe to index with.
    19. Check for a fresh orange at that position.
    20. Rot it: set the cell to 2. Marking it immediately stops any other orange from rotting it again or queueing it twice.
    21. Subtract 1 from fresh.
    22. Push the neighbour onto the back of the queue, behind this minute's oranges.
    23. After the loop, return -1 if fresh > 0. The queue ran out while oranges were still fresh.
    24. Return minutes as the last expression.

    The solution in Rust

    use std::collections::VecDeque;
    
    impl Solution {
        fn oranges_rotting(grid: Vec<Vec<i32>>) -> i32 {
            let mut grid = grid;
            let rows = grid.len();
            let cols = grid[0].len();
            let mut queue: VecDeque<(usize, usize)> = VecDeque::new();
            let mut fresh = 0;
            for r in 0..rows {
                for c in 0..cols {
                    if grid[r][c] == 2 {
                        queue.push_back((r, c));
                    }
                    if grid[r][c] == 1 {
                        fresh += 1;
                    }
                }
            }
            let directions: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
            let mut minutes = 0;
            while !queue.is_empty() && fresh > 0 {
                minutes += 1;
                let level_size = queue.len();
                for _ in 0..level_size {
                    let (row, col) = queue.pop_front().unwrap();
                    for &(dr, dc) in &directions {
                        let next_row = row as i32 + dr;
                        let next_col = col as i32 + dc;
                        if next_row >= 0 && next_row < rows as i32 && next_col >= 0 && next_col < cols as i32 {
                            let (next_row, next_col) = (next_row as usize, next_col as usize);
                            if grid[next_row][next_col] == 1 {
                                grid[next_row][next_col] = 2;
                                fresh -= 1;
                                queue.push_back((next_row, next_col));
                            }
                        }
                    }
                }
            }
            if fresh > 0 {
                return -1;
            }
            minutes
        }
    }
    

    The Rust details worth a closer look:

    • Mutating the grid. A function parameter is immutable unless declared otherwise, and the signature takes grid by value. let mut grid = grid; moves it into a mutable binding of the same name, so the original parameter can no longer be used.
    • Two integer types for positions. Vector indexes must be usize, but a neighbour one step off the grid is -1, and a usize can't hold that: 0usize - 1 panics in a debug build and wraps to a huge number in the -O builds the lessons use. So the arithmetic is done in i32, the bounds are checked, and only then is the position converted with as usize.
    • Shadowing. let (next_row, next_col) = (next_row as usize, next_col as usize); declares new variables with the same names and the index type. The old i32 variables are no longer reachable inside that block.
    • for &(dr, dc) in &directions. Iterating over a reference to the array yields references to the tuples. The pattern &(dr, dc) copies the two numbers out of each.
    • pop_front() returns an Option. unwrap() is safe because the loop pops only level_size elements, which the queue holds.
    • The inferred type of fresh and minutes. They start as 0, and the function returns minutes as an i32, so both are i32.

    Complexity

    Time: O(m · n). The first pass visits each cell once. In the search, each cell enters the queue at most once and looks at 4 neighbours, so the total is a constant times the number of cells.

    Space: O(m · n). In the worst case, such as a grid full of rotten oranges, the queue holds every cell. The grid itself is changed in place.

    Edge cases and common mistakes

    • No fresh oranges. [[0, 2]] returns 0, and so do [[2]] and [[0]]. The fresh > 0 test stops the loop before the first minute.
    • No rotten oranges. [[1]] returns -1: the queue is empty and one orange is fresh.
    • Unreachable oranges. In the second example the BFS ends with fresh = 1, and the function returns -1. The loop does run a minute in which nothing rots, but the -1 return hides it.
    • A single row. [[2, 1, 1, 1, 1]] takes 4 minutes: the rot moves one cell per minute along the row.
    • Counting minutes per orange. Adding 1 for every orange taken from the queue counts oranges, not minutes. That is why the loop takes exactly level_size oranges and then adds one minute for the whole level.
    • Not marking at once. If the cell were marked only when popped, the same orange could be queued by several neighbours and fresh would go down several times for it.
    • Diagonal spread. Adding diagonal moves to directions would change the answers: the bottom-left orange of the second example would rot.

    Testing

    • [[2, 1, 1], [1, 1, 0], [0, 1, 1]] gives 4.
    • [[2, 1, 1], [0, 1, 1], [1, 0, 1]] gives -1.
    • [[0, 2]] gives 0.
    • [[1]] gives -1.
    • [[2]] gives 0.
    • [[2, 1, 1, 1, 1]] gives 4.
    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free