title: Rotting Oranges
slug: rotting-oranges
difficulty: medium
leetcode_id: 994
leetcode_url: https://leetcode.com/problems/rotting-oranges/
categories:
  - arrays
  - graphs
patterns:
  - bfs
  - matrix-traversal

function_signature: "def oranges_rotting(grid: list[list[int]]) -> int:"

test_cases:
  visible:
    - input: { grid: [[2, 1, 1], [1, 1, 0], [0, 1, 1]] }
      expected: 4
    - input: { grid: [[2, 1, 1], [0, 1, 1], [1, 0, 1]] }
      expected: -1
    - input: { grid: [[0, 2]] }
      expected: 0
  hidden:
    - input: { grid: [[0]] }
      expected: 0
    - input: { grid: [[1]] }
      expected: -1
    - input: { grid: [[2, 1, 1], [1, 1, 1], [1, 1, 2]] }
      expected: 2
    - input: { grid: [[2]] }
      expected: 0
    - input: { grid: [[1, 2]] }
      expected: 1
    - input: { grid: [[2, 1, 1, 1, 1]] }
      expected: 4

description: |
  You are given an `m x n` `grid` where each cell can have one of three values:

  - `0` representing an empty cell,
  - `1` representing a fresh orange, or
  - `2` representing a rotten orange.

  Every minute, any fresh orange that is **4-directionally adjacent** to a rotten orange becomes rotten.

  Return *the minimum number of minutes that must elapse until no cell has a fresh orange*. If *this is impossible, return* `-1`.

constraints: |
  - `m == grid.length`
  - `n == grid[i].length`
  - `1 <= m, n <= 10`
  - `grid[i][j]` is `0`, `1`, or `2`

examples:
  - input: "grid = [[2,1,1],[1,1,0],[0,1,1]]"
    output: "4"
    explanation: "Starting from the rotten orange at position (0,0), adjacent oranges rot each minute. After 4 minutes, all reachable fresh oranges become rotten."
  - input: "grid = [[2,1,1],[0,1,1],[1,0,1]]"
    output: "-1"
    explanation: "The orange in the bottom left corner (row 2, column 0) is never rotten, because rotting only happens 4-directionally and it's isolated by the empty cell."
  - input: "grid = [[0,2]]"
    output: "0"
    explanation: "Since there are already no fresh oranges at minute 0, the answer is just 0."

explanation:
  intuition: |
    Imagine the rotting process spreading like ripples in a pond. Each rotten orange is a stone dropped in the water, and the "rot" spreads outward one cell at a time in all four directions (up, down, left, right).

    The key insight is that **all rotten oranges spread simultaneously**. This isn't a single-source problem where rot spreads from one orange — it's a **multi-source BFS** where all initially rotten oranges act as starting points at the same time.

    Think of it like this: at minute 0, all initially rotten oranges "infect" their adjacent fresh oranges. At minute 1, all those newly rotten oranges infect *their* adjacent fresh oranges. This level-by-level expansion is exactly what BFS does naturally.

    The answer is the number of "levels" or "waves" of BFS until no fresh oranges remain. If any fresh orange is unreachable (isolated by empty cells or grid boundaries), we return `-1`.

  approach: |
    We solve this using a **Multi-Source BFS** approach:

    **Step 1: Initialise the queue and count fresh oranges**

    - Scan the entire grid once
    - Add all rotten oranges (value `2`) to a queue — these are our starting points
    - Count all fresh oranges (value `1`) — we'll decrement this as oranges rot
    - `fresh_count`: Tracks remaining fresh oranges
    - `minutes`: Tracks elapsed time (starts at `0`)

    &nbsp;

    **Step 2: Perform BFS level by level**

    - While the queue is not empty and fresh oranges remain:
      - Record the current queue size — this is the number of oranges rotting at this "level"
      - Process all oranges at the current level
      - For each rotten orange, check all 4 adjacent cells (up, down, left, right)
      - If an adjacent cell contains a fresh orange, rot it (change to `2`), add to queue, decrement `fresh_count`
      - After processing the entire level, increment `minutes`

    &nbsp;

    **Step 3: Check for unreachable oranges**

    - If `fresh_count > 0` after BFS completes, some oranges were unreachable — return `-1`
    - Otherwise, return `minutes`

    &nbsp;

    The BFS guarantees we find the minimum time because we explore all oranges at distance `d` before any orange at distance `d+1`.

  common_pitfalls:
    - title: Single-Source Instead of Multi-Source BFS
      description: |
        A common mistake is to run BFS from each rotten orange separately and take the maximum. This is both inefficient and incorrect.

        The correct approach is to add **all** initially rotten oranges to the queue before starting BFS. This simulates the simultaneous spread from all sources.

        With multi-source BFS, we process all "distance-1" oranges before any "distance-2" oranges, naturally giving us the minimum time.
      wrong_approach: "Run separate BFS from each rotten orange"
      correct_approach: "Add all rotten oranges to queue initially (multi-source BFS)"

    - title: Forgetting Edge Cases
      description: |
        Don't forget to handle these scenarios:

        - **No fresh oranges**: Return `0` immediately — nothing needs to rot
        - **No rotten oranges but fresh oranges exist**: Return `-1` — rot can never spread
        - **Isolated fresh oranges**: Fresh oranges surrounded by empty cells or walls can never rot

        The `fresh_count` check at the end handles all unreachable cases.

    - title: Off-by-One in Time Counting
      description: |
        Be careful when counting minutes. The BFS starts at minute `0` with the initial rotten oranges. Each "wave" of new rotting takes one minute.

        Only increment `minutes` when you actually rot new oranges. If you increment before checking if any rotting happened, you'll get an incorrect answer when the grid starts with no fresh oranges.

  key_takeaways:
    - "**Multi-source BFS**: When spreading from multiple starting points simultaneously, initialise the queue with all sources before starting BFS"
    - "**Level-order processing**: Process BFS level-by-level to track the number of steps/waves/minutes"
    - "**Grid traversal pattern**: The 4-directional adjacency `[(0,1), (0,-1), (1,0), (-1,0)]` is a common pattern for matrix problems"
    - "**Reachability check**: Track unvisited targets to detect impossible cases — BFS naturally handles this"

  time_complexity: "O(m × n). We visit each cell at most twice — once during initial scan, once during BFS."
  space_complexity: "O(m × n). In the worst case, all cells are rotten and stored in the queue."

solutions:
  - approach_name: Multi-Source BFS
    is_optimal: true
    code: |
      from collections import deque

      def oranges_rotting(grid: list[list[int]]) -> int:
          rows, cols = len(grid), len(grid[0])
          queue = deque()
          fresh_count = 0

          # Step 1: Find all rotten oranges and count fresh ones
          for r in range(rows):
              for c in range(cols):
                  if grid[r][c] == 2:
                      queue.append((r, c))  # Add rotten orange to queue
                  elif grid[r][c] == 1:
                      fresh_count += 1  # Count fresh oranges

          # Edge case: no fresh oranges to begin with
          if fresh_count == 0:
              return 0

          # Directions for 4-directional movement
          directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
          minutes = 0

          # Step 2: BFS level by level
          while queue and fresh_count > 0:
              minutes += 1  # Each level = 1 minute
              # Process all oranges at current level
              for _ in range(len(queue)):
                  r, c = queue.popleft()
                  # Check all 4 adjacent cells
                  for dr, dc in directions:
                      nr, nc = r + dr, c + dc
                      # If valid cell with fresh orange, rot it
                      if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
                          grid[nr][nc] = 2  # Mark as rotten
                          fresh_count -= 1
                          queue.append((nr, nc))

          # Step 3: Check if all oranges rotted
          return minutes if fresh_count == 0 else -1
    explanation: |
      **Time Complexity:** O(m × n) — Each cell is visited at most once during BFS.

      **Space Complexity:** O(m × n) — Queue can hold all cells in the worst case.

      We use multi-source BFS where all initially rotten oranges spread simultaneously. By processing level-by-level, we naturally count the minimum minutes needed. The `fresh_count` tracker lets us detect unreachable oranges.

  - approach_name: Simulation (Brute Force)
    is_optimal: false
    code: |
      def oranges_rotting(grid: list[list[int]]) -> int:
          rows, cols = len(grid), len(grid[0])
          directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
          minutes = 0

          def count_fresh():
              """Count remaining fresh oranges."""
              count = 0
              for r in range(rows):
                  for c in range(cols):
                      if grid[r][c] == 1:
                          count += 1
              return count

          # Simulate until no changes occur
          while True:
              fresh_before = count_fresh()
              if fresh_before == 0:
                  return minutes

              # Find all oranges that will rot this minute
              to_rot = []
              for r in range(rows):
                  for c in range(cols):
                      if grid[r][c] == 1:  # Fresh orange
                          # Check if adjacent to rotten
                          for dr, dc in directions:
                              nr, nc = r + dr, c + dc
                              if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 2:
                                  to_rot.append((r, c))
                                  break

              # If nothing can rot, but fresh remain, impossible
              if not to_rot:
                  return -1

              # Rot the oranges
              for r, c in to_rot:
                  grid[r][c] = 2

              minutes += 1
    explanation: |
      **Time Complexity:** O((m × n)²) — Each minute we scan the entire grid, and there can be O(m × n) minutes.

      **Space Complexity:** O(m × n) — We store oranges to rot each round.

      This approach simulates the process directly: each minute, scan the grid, find fresh oranges adjacent to rotten ones, and rot them. While correct, it's inefficient because we repeatedly scan cells that won't change. The BFS approach is preferred.
