Skip to content

learn: solve Rotting Oranges (#994) via guided TDD#231

Merged
pertrai1 merged 6 commits into
mainfrom
tdd/0994-rotting-oranges
Feb 11, 2026
Merged

learn: solve Rotting Oranges (#994) via guided TDD#231
pertrai1 merged 6 commits into
mainfrom
tdd/0994-rotting-oranges

Conversation

@pertrai1

@pertrai1 pertrai1 commented Feb 11, 2026

Copy link
Copy Markdown
Owner

Problem

994 - Rotting Oranges — Medium

Approach

Multi-source BFS starting from all initially rotten oranges, processing level-by-level where each level represents one minute. Track fresh orange count and return -1 if any remain after BFS completes.

Complexity

  • Time: O(m × n) where m = rows, n = columns
  • Space: O(m × n) for the queue in worst case

Pattern

Multi-Source Breadth-First Search (BFS)

Learning Summary

  • Mode: Coach (learner wrote all implementation code)
  • Cycles: 4 guided cycles
  • Tests: 4 test cases (no oranges, only rotten, basic rotting, impossible case)
  • Hint Levels Used: Mostly L1 (guiding questions), L2 for bounds checking and level-tracking pattern
  • Key Insight: BFS processes nodes level-by-level; snapshotting queue size ensures each level = one minute
  • Pattern Checkpoints: 1/1 correct on first attempt (identified BFS), needed follow-up on state variables

🧠 Self-Review Questions

  1. Why do we snapshot the queue size (levelSize = queue.length) before processing each level?
Answer To ensure we only process oranges at the current time/distance level. Without snapshotting, newly-added oranges from the current level would be processed immediately, breaking the level-by-level invariant. The snapshot ensures all oranges at distance D rot before any at distance D+1.
  1. Why do we need to mark oranges as rotten in the grid (grid[newRow][newCol] = 2) when adding them to the queue?
Answer To prevent the same orange from being added to the queue multiple times. Without marking, if an orange has multiple rotten neighbors, it could be added once per neighbor, causing duplicate processing and incorrect minute counting.

Files

  • README.md — Problem statement
  • rotting-oranges.ts — Solution
  • rotting-oranges.test.ts — Test suite
  • POST_MORTEM.md — Interview debrief and retrospective

Summary by CodeRabbit

  • New Features

    • Added LeetCode problem 0994 - Rotting Oranges with a complete solution.
  • Documentation

    • Added problem README with examples and constraints.
    • Added a detailed post-mortem / interview debrief.
    • Updated problems index to include 0994 in relevant sections.
  • Tests

    • Added unit tests covering multiple scenarios and edge cases.

@pertrai1 pertrai1 added the code challenge this is a coding challenge label Feb 11, 2026
@pertrai1

Copy link
Copy Markdown
Owner Author

💡 Educational Note - Level-by-Level BFS

The key to this solution is on lines 20-21:

const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) { ... }

Why this works: By snapshotting the queue size before processing, we ensure we only process oranges at the current time level. Without this snapshot, newly-added oranges would be processed immediately, breaking the invariant that "each level = one minute".

Time Complexity: O(m × n) where m = rows, n = columns. Each cell is visited at most once (added to queue once, marked rotten once).

Space Complexity: O(m × n) worst case when all cells are initially rotten and added to the queue simultaneously.

@github-actions

github-actions Bot commented Feb 11, 2026

Copy link
Copy Markdown

Claude finished @pertrai1's task in 1m 27s —— View job


📋 Review Checklist

  • Read all changed files
  • Verify algorithmic correctness
  • Validate complexity analysis
  • Assess code quality
  • Provide pattern identification and learning insights
  • Suggest related problems

✅ Correctness Review

The solution is algorithmically correct and handles all required cases properly:

Edge Cases Handled:

  • Empty grid / no fresh oranges (returns 0)
  • All oranges already rotten (returns 0)
  • Impossible to rot all oranges (returns -1)
  • Multi-source BFS starting from multiple rotten oranges

Test Coverage:
The test suite covers all critical scenarios. Mental trace through Example 1:

  • Grid: [[2,1,1],[1,1,0],[0,1,1]]
  • Initial: 5 fresh oranges, queue has [[0,0]]
  • Level 0: Rots (1,0) and (0,1) → 3 fresh, queue size 2
  • Level 1: Rots (2,0), (1,1), (0,2) → 0 fresh, queue size 3
  • Level 2: Rots (2,1) → 0 fresh, queue size 1
  • Level 3: Rots (2,2) → 0 fresh, queue empty
  • Result: 4 minutes ✓

📊 Complexity Analysis

Time Complexity: O(m × n) ✅ Accurate

  • Each cell is visited at most once (added to queue once, marked rotten once)
  • Initialization loop: O(m × n)
  • BFS processes each cell at most once: O(m × n)
  • Total: O(m × n)

Space Complexity: O(m × n) ✅ Accurate

  • Worst case: All cells are initially rotten → entire grid added to queue
  • Average case: Usually much smaller (only rotten oranges in queue)
  • Grid is modified in-place (no extra space for visited tracking)

The author's complexity analysis in the PR description is 100% correct.


🎯 Pattern Recognition

Pattern: Multi-Source Breadth-First Search (BFS)

Why This Pattern Works:
This is a classic "shortest distance from multiple sources" problem. Multi-source BFS is the perfect pattern because:

  1. BFS guarantees shortest path — processes nodes in increasing distance from sources
  2. Level-by-level processing maps to time — each BFS level = 1 minute elapsed
  3. Multiple starting points — all initially rotten oranges are sources
  4. 4-directional propagation — natural fit for BFS neighbor exploration

Key Insight (The "Aha!" Moment):
The critical technique is snapshotting the queue size before processing each level (rotting-oranges.ts:20-21):

const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) { ... }

Why this works: Without the snapshot, newly-added oranges from the current level would be processed immediately, breaking the invariant that "each complete BFS level = exactly 1 minute". The snapshot ensures all oranges at distance D rot before any at distance D+1.


💎 Code Quality

Strengths:

  • Excellent variable namingfreshCount, levelSize, queue are all clear and semantic
  • Clean structure — three distinct phases (initialization, BFS, return) with clear comments
  • Proper bounds checking — all four directions check the new position before accessing
  • Efficient state tracking — uses grid modification (grid[row][col] = 2) to mark visited, avoiding a separate visited set
  • Correct minute counting — only increments minutes if queue has remaining elements (rotting-oranges.ts:48-50)

Minor Suggestions (Not Issues):

  1. Consider extracting directions array (optional for readability):

    const directions = [[1,0], [-1,0], [0,1], [0,-1]];
    for (const [dr, dc] of directions) {
      const newRow = row + dr, newCol = col + dc;
      if (newRow >= 0 && newRow < grid.length && 
          newCol >= 0 && newCol < grid[0].length && 
          grid[newRow][newCol] === 1) {
        // ... rot the orange
      }
    }

    Trade-off: The current explicit approach is more beginner-friendly and easier to debug. The directions array is more concise but adds abstraction. Both are equally valid. ✅

  2. Type annotation consistency: The function signature uses number[][] for grid, and internal variables use : number — this is consistent and correct. ✅


🎓 Learning Summary

What Makes This Solution Elegant

  1. Level-by-level invariant — The snapshot pattern (lines 20-21) elegantly guarantees that each complete level = 1 minute
  2. In-place modification — Marking oranges rotten in the grid saves space and prevents duplicate processing
  3. Fresh count optimization — Tracking freshCount allows O(1) final check instead of re-scanning the grid

Common Mistakes with This Pattern

Forgetting to snapshot queue size:

// WRONG: Processes newly-added oranges immediately
while (queue.length > 0) {
  const [row, col] = queue.shift()!;
  // ... adds neighbors to queue
  minutes++; // ❌ Increments per orange, not per level
}

Not marking oranges as rotten when adding to queue:

// WRONG: Same orange added multiple times
if (grid[newRow][newCol] === 1) {
  queue.push([newRow, newCol]);
  // ❌ Missing: grid[newRow][newCol] = 2
}

Incrementing minutes unconditionally:

// WRONG: Adds extra minute when queue becomes empty
while (queue.length > 0) {
  // ... process level
  minutes++; // ❌ Should check if queue.length > 0 first
}

Your solution avoids all of these pitfalls. ✅


🔗 Related Problems to Practice

Same Pattern (Multi-Source BFS):

  1. 286 - Walls and Gates (Medium) 🔒
    Start from all gates (value 0) and fill distances to empty rooms using multi-source BFS. Identical pattern to Rotting Oranges.

  2. 542 - 01 Matrix (Medium)
    Find nearest 0 for each cell. Multi-source BFS starting from all 0s. Same "distance from multiple sources" concept.

  3. 1162 - As Far from Land as Possible (Medium)
    Find water cell farthest from land. Multi-source BFS from all land cells, track maximum distance.

Level-by-Level BFS Practice:

  1. 1091 - Shortest Path in Binary Matrix (Medium)
    Single-source BFS with 8-directional movement. Reinforces level tracking and bounds checking.

  2. 1429 - First Unique Number (Medium) 🔒
    Not BFS, but practices queue-based level processing with invariants.

Why These Help:

  • Walls and Gates (#286) — Nearly identical to Rotting Oranges, great for muscle memory
  • 01 Matrix (#542) — Same pattern, different domain (reinforces multi-source intuition)
  • Shortest Path in Binary Matrix (#1091) — Single-source variant, practices bounds checking

🎯 Final Assessment

Overall: This is an exemplary TDD-driven solution with excellent code quality, correct complexity analysis, and comprehensive test coverage.

Highlights:

  • ✅ Clean, readable implementation with semantic naming
  • ✅ Correct algorithmic approach (multi-source BFS)
  • ✅ Accurate complexity analysis (O(m × n) time and space)
  • ✅ All edge cases covered in tests
  • ✅ Excellent POST_MORTEM.md documenting the learning journey
  • ✅ Self-review questions in PR description demonstrate deep understanding

Learning Value: The level-by-level BFS pattern you've mastered here is foundational for dozens of graph/grid problems. The snapshot technique (lines 20-21) is a game-changer for any "time-based propagation" problem.

Recommendation:Approve and merge

Great work on this solution! The TDD approach clearly helped build a robust, well-tested implementation. 🎉


@coderabbitai

coderabbitai Bot commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds LeetCode problem 0994 (Rotting Oranges): a BFS-based solution, unit tests, problem README, interview post-mortem, and two references inserted into the main problems index.

Changes

Cohort / File(s) Summary
Problem Index & Docs
docs/PROBLEMS.md
Inserts 0994 - Rotting Oranges into Medium problems lists (Graph & Grid and Graphs/BFS sections).
Problem Documentation
leetcode/medium/0994-rotting-oranges/README.md, leetcode/medium/0994-rotting-oranges/POST_MORTEM.md
Adds problem README with examples and constraints, and a detailed interview post-mortem documenting approach, scoring, and notes.
Implementation & Tests
leetcode/medium/0994-rotting-oranges/rotting-oranges.ts, leetcode/medium/0994-rotting-oranges/rotting-oranges.test.ts
Adds orangesRotting BFS implementation and Vitest unit tests covering empty grid, all-rotten, typical progression, and impossible cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through grids of orange light,

Queue full, minutes ticking bright,
Neighbors fall, one by one,
BFS finished—work well done! 🍊✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'learn: solve Rotting Oranges (#994) via guided TDD' clearly and concisely summarizes the main change: solving LeetCode problem 994 using test-driven development, which aligns with the PR objectives and changeset contents.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tdd/0994-rotting-oranges

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pertrai1

Copy link
Copy Markdown
Owner Author

💡 Educational Note - Bounds Checking Pattern

Lines 27, 33, 39, 45 demonstrate the critical bounds-checking pattern for grid problems:

if (row + 1 < grid.length && grid[row + 1][col] === 1) { ... }
if (row - 1 >= 0 && grid[row - 1][col] === 1) { ... }
if (col + 1 < grid[0].length && grid[row][col + 1] === 1) { ... }
if (col - 1 >= 0 && grid[row][col - 1] === 1) { ... }

The pattern: Check the new position before accessing it:

  • For row + 1: check row + 1 < grid.length
  • For row - 1: check row - 1 >= 0
  • For col + 1: check col + 1 < grid[0].length
  • For col - 1: check col - 1 >= 0

Common mistake: Checking the current position (row >= 0 && row < grid.length) instead of the new position you're about to access.

@pertrai1

Copy link
Copy Markdown
Owner Author

💡 Educational Note - Marking Visited Cells

Lines 30, 36, 42, 48 mark oranges as rotten when adding them to the queue:

grid[row + 1][col] = 2;  // Mark as rotten immediately

Why this is essential: Without marking, the same orange could be added multiple times if it has multiple rotten neighbors. For example, if an orange at (2,2) is adjacent to rotten oranges at (1,2) and (2,1), it would be added twice without marking, causing:

  • Duplicate queue entries
  • Incorrect minute counting (processing the same orange multiple times)

The pattern: In BFS/DFS on grids, mark cells as visited when adding to queue, not when processing. This prevents duplicates.

@github-actions

github-actions Bot commented Feb 11, 2026

Copy link
Copy Markdown

✅ Test Results

Tests have been executed for this pull request.

📊 Coverage Summary

Metric Coverage
Statements 93.96%
Branches 90.47%
Functions 87.23%
Lines 93.88%

Check the workflow logs for detailed test output.

@github-actions

Copy link
Copy Markdown

🤖 Gemini Code Review (Model: gemini-2.5-flash)

Here's a detailed review of the Rotting Oranges solution:


1. Pattern Identification

This solution uses a Multi-Source Breadth-First Search (BFS).

  • BFS: It's a graph traversal algorithm that explores all the neighbor nodes at the present depth before moving on to the nodes at the next depth level. This level-by-level exploration is crucial for problems asking for the "minimum steps" or "shortest time," as each level processed in BFS corresponds to one unit of time (in this case, one minute).
  • Multi-Source: The rotting process starts simultaneously from all initially rotten oranges. Therefore, the BFS queue is initialized with all cells containing rotten oranges, and the search expands outwards from all these sources concurrently.

2. Complexity Verification

  • Time Complexity: O(m * n)

    • Verification: The initial loops iterate through all m * n cells of the grid once to find all initially rotten oranges and count all fresh oranges. This takes O(m * n) time.
    • The BFS itself processes each cell in the grid at most once. When an orange rots and is added to the queue, it's marked as rotten (grid[newRow][newCol] = 2), preventing it from being added again. Each time a cell is dequeued, its four neighbors are checked. Since each cell is processed once, and each edge (connection to a neighbor) is effectively traversed a constant number of times (4 directions), the BFS part also takes O(m * n) time.
    • Combining these, the overall time complexity is O(m * n), where m is the number of rows and n is the number of columns.
    • Correction/Clarification: The POST_MORTEM.md mentions O(n) which is a bit vague for a 2D grid. The correct and more precise complexity is O(m * n).
  • Space Complexity: O(m * n)

    • Verification: In the worst-case scenario, the queue could potentially store all cells in the grid. For instance, if the entire grid is initially fresh oranges with a single rotten orange, the queue could grow to hold almost all m * n cells as they progressively rot.
    • The grid is modified in-place, so it doesn't count as additional space.
    • Therefore, the space complexity is O(m * n).

3. Key Insight

The key insight for this problem is that the spreading of rot occurs in discrete "minutes," and we need the minimum total minutes. This structure inherently suggests a Breadth-First Search (BFS). The "aha!" moment comes from realizing that:

  1. Multi-Source Start: All initially rotten oranges start rotting their neighbors at the same time. So, all these initial rotten oranges must be added to the BFS queue at the beginning.
  2. Level-by-Level Time Tracking: BFS naturally processes nodes layer by layer. If we process all oranges that rot in minute X before moving on to any oranges that rot in minute X+1, we can correctly track the elapsed time. This is achieved by taking a "snapshot" of the queue's size (levelSize) at the beginning of each minute and processing only those levelSize elements, then incrementing the minutes counter.
  3. Fresh Orange Count: Keeping a freshCount is essential to determine if it's impossible to rot all oranges. If freshCount is greater than 0 after the BFS completes, it means some fresh oranges were unreachable and will never rot, in which case we return -1.

4. Edge Case Analysis

The current solution handles the following edge cases correctly:

  • No oranges at all (e.g., [[0,0]]): freshCount remains 0, queue is empty. The while loop condition (queue.length > 0) is immediately false. Returns 0. (Correct, as no time is needed).
  • Only rotten oranges (e.g., [[2]]): freshCount remains 0. The rotten orange is added to the queue. The while loop runs for one level, levelSize is 1. No fresh oranges are found, queue becomes empty. minutes remains 0 because the if (queue.length > 0) check prevents incrementing minutes if no new oranges were added to the queue in the last level. Returns 0. (Correct).
  • No rotten oranges initially, but fresh ones exist (e.g., [[1,1],[1,1]]): freshCount will be > 0. The queue will be empty after initialization. The while loop never runs. The final check freshCount > 0 ? -1 : minutes returns -1. (Correct, as nothing can start the rotting process).
  • Impossible to rot all oranges (e.g., a fresh orange is isolated or in a separate component): freshCount will be greater than 0 after the while loop finishes. The solution correctly returns -1.
  • Single cell grid:
    • [[0]]: Returns 0.
    • [[1]]: Returns -1.
    • [[2]]: Returns 0.
      All these small cases are handled correctly.

The solution appears robust for the given constraints.

5. Learning Points

  • Similar problems that use this pattern:
    • 286 - Walls and Gates: Given a 2D grid with walls, gates, and empty rooms, fill each empty room with the distance to its nearest gate. This is a classic multi-source BFS problem.
    • 542 - 01 Matrix: Given a binary matrix, find the distance of the nearest 0 for each cell. Another perfect application of multi-source BFS.
    • 1091 - Shortest Path in Binary Matrix: Find the shortest clear path in a binary matrix from top-left to bottom-right. While single-source, it reinforces BFS for shortest paths and grid traversal.
  • Common mistakes people make with this pattern:
    • Incorrectly tracking time/levels: Not using the levelSize variable to process all nodes at the current depth before incrementing the time counter. This leads to incorrect minimum time calculations. The if (queue.length > 0) check before incrementing minutes is also crucial for the last minute.
    • Forgetting to count fresh items: Not initializing freshCount or not decrementing it when a fresh item is processed. This leads to incorrect -1 returns when all items could have rotted or incorrect 0 returns when some remain.
    • Incorrect boundary checks: Failing to validate newRow and newCol are within the grid dimensions before accessing grid[newRow][newCol].
    • Re-processing visited nodes: Not marking visited/rotted cells (either by modifying the grid or using a visited set) can lead to infinite loops or inefficient re-processing.
  • Variations of this problem:
    • Different adjacency rules: Instead of 4-directional, it could be 8-directional (including diagonals).
    • Obstacles: Some cells might be impassable "walls" (0 in this problem is empty, not an obstacle) that rot cannot cross.
    • Weighted rotting time: Each cell might take a different amount of time to rot, or to rot its neighbors (would require Dijkstra's algorithm for minimum time).
    • Multiple types of "spread": Different types of oranges might spread rot differently.

6. Code Quality

The code quality is excellent:

  • Variable Naming: freshCount, minutes, queue, levelSize, row, col are all highly descriptive and follow common conventions.
  • Code Structure: The solution is well-structured, with a clear initialization phase, a main BFS loop, and a concise final return statement.
  • Readability: The use of const for levelSize and destructuring for [row, col] enhances readability. The queue.shift()! is acceptable given the queue.length > 0 check. Comments clearly explain the purpose of different sections.

Minor Improvement Suggestion (for conciseness and flexibility):

Instead of four separate if statements for checking neighbors, you can use deltaRow and deltaCol arrays. This makes the code more compact and easier to adapt if the adjacency rules change (e.g., to 8-directional).

export function orangesRotting(grid: number[][]): number {
  let freshCount: number = 0;
  let minutes = 0;

  const queue: number[][] = [];
  const rows = grid.length;
  const cols = grid[0].length;

  // Directions for 4-directional movement: up, down, left, right
  const directions = [
    [-1, 0], // Up
    [1, 0],  // Down
    [0, -1], // Left
    [0, 1]   // Right
  ];

  // Initialize queue with all initially rotten oranges and count fresh oranges
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      if (grid[row][col] === 2) {
        queue.push([row, col]);
      } else if (grid[row][col] === 1) {
        freshCount++;
      }
    }
  }

  // BFS: Process oranges level-by-level (each level = 1 minute)
  while (queue.length > 0) {
    const levelSize = queue.length;

    // Only increment minutes AFTER a level has been processed and if there are
    // still oranges in the queue to process in the next minute.
    // If queue becomes empty here, it means no new oranges rotted in the last minute.
    if (freshCount > 0 && queue.length > 0) { // Only increment if there were fresh oranges to rot AND new rotten oranges were added to queue
      minutes++;
    }

    for (let i = 0; i < levelSize; i++) {
      const [row, col] = queue.shift()!;

      for (const [dr, dc] of directions) {
        const newRow = row + dr;
        const newCol = col + dc;

        // Check bounds and if the neighbor is a fresh orange
        if (
          newRow >= 0 && newRow < rows &&
          newCol >= 0 && newCol < cols &&
          grid[newRow][newCol] === 1
        ) {
          queue.push([newRow, newCol]);
          grid[newRow][newCol] = 2; // Mark as rotten
          freshCount--;
        }
      }
    }
  }

  return freshCount > 0 ? -1 : minutes;
}

Self-correction on minutes increment: The original code increments minutes after a level is processed, but before checking if freshCount is 0. My suggested change if (freshCount > 0 && queue.length > 0) for minutes++ might be slightly off. The original if (queue.length > 0) check is correct: it means "if there are still oranges in the queue for the next minute, then a minute has elapsed." If queue.length is 0 here, it means the last batch of rotten oranges didn't rot any new ones, so no additional minute passes. The original code is correct for minutes increment. My proposed freshCount > 0 condition is redundant and potentially incorrect for cases where freshCount might have already reached zero before the last minute increment. Stick to the original.

The original if (queue.length > 0) for minutes++ is correct because it signifies that the current level successfully rotted at least one new orange, thus initiating a next minute. If queue.length is 0 after processing a level, it means no new oranges rotted, and thus no further minutes elapse.

7. Alternative Approaches

  1. DFS (Depth-First Search): While DFS can explore a grid, it's generally not ideal for "shortest path" or "minimum time" problems because it explores one path to its maximum depth before backtracking. To use DFS for this problem, you would need to track the minimum time for each orange to rot and potentially use memoization, making the implementation more complex than BFS. It also wouldn't naturally give the level-by-level time progression that BFS provides. The worst-case time and space complexity would likely remain O(m * n), but with larger constant factors and potential stack overflow issues for very large grids due to recursion depth.
  2. BFS with a separate visited array: Instead of modifying the input grid to mark oranges as rotten (grid[newRow][newCol] = 2), one could use a separate boolean[][] visited array or Set<string> to keep track of processed oranges. This approach keeps the original grid immutable. The trade-off is slightly increased memory usage for the visited array/set (still O(m * n)) and potentially slightly slower lookups if using a Set with string keys for coordinates. For this problem, modifying the grid in place is a common and efficient practice.

@github-actions

Copy link
Copy Markdown

🤖 Gemini Code Review (Model: gemini-2.5-flash)

Here's a review of the Rotting Oranges solution, structured to provide educational feedback and highlight key DSA concepts.


1. Pattern Identification

This solution uses Multi-Source Breadth-First Search (BFS).

  • Why BFS? The problem asks for the minimum time for oranges to rot. BFS is ideal for finding the shortest path or minimum steps in an unweighted graph because it explores all nodes at a given "distance" (or in this case, "minute") from the source(s) before moving to the next distance/minute.
  • Multi-Source: The rotting process starts simultaneously from all initially rotten oranges. BFS naturally handles this by initializing the queue with all such "source" nodes.

2. Complexity Verification

  • Time Complexity: O(m * n)
    • Verification: This is accurate. The initialization loop iterates through all m * n cells once. The BFS while loop processes each cell at most once: a cell is added to the queue only when it turns rotten, and once processed, it's marked as rotten (grid[row][col] = 2), preventing it from being added again. Each cell's neighbors are checked a constant number of times (4 directions). Therefore, the total time complexity is proportional to the number of cells in the grid.
  • Space Complexity: O(m * n)
    • Verification: This is accurate. In the worst case (e.g., all oranges are initially rotten, or a large perimeter of fresh oranges rots simultaneously), the queue could hold up to O(m * n) elements. Each element stores a [row, col] pair. The grid is modified in-place, so it doesn't contribute to additional space complexity beyond the input itself.

3. Key Insight

The key insight for solving Rotting Oranges efficiently is to treat each "minute" as a level in a BFS traversal.

  1. Multi-Source Start: Instead of starting from a single point, initialize the BFS queue with all initially rotten oranges.
  2. Level-by-Level Processing: To accurately count minutes, you must process all oranges that become rotten in a given minute before incrementing the minute counter. This is achieved by taking a "snapshot" of the queue's size (levelSize) at the beginning of each BFS level. All oranges dequeued within that levelSize are considered to have rotted in the same minute. Once levelSize oranges are processed, the minutes counter is incremented (if new oranges were added to the queue).
  3. State Tracking: Simultaneously track the freshCount of oranges. If freshCount is still greater than zero after the BFS completes, it means some fresh oranges were unreachable, and the result should be -1.

This level-by-level processing ensures that the minutes variable correctly reflects the minimum time, as BFS inherently finds the shortest path/time in an unweighted graph.

4. Edge Case Analysis

The current solution handles the following critical edge cases correctly:

  • No oranges at all ([[0,0]]): freshCount remains 0, queue is empty, minutes is 0. Returns 0.
  • Only rotten oranges ([[2]]): freshCount remains 0, queue contains the rotten orange. The loop runs, minutes remains 0 (because queue.length will be 0 after the first level), returns 0.
  • Only fresh oranges, no initial rotten oranges ([[1]] or [[1,1],[1,1]]): freshCount is > 0, queue is empty initially. The while loop never runs. Returns -1.
  • Impossible to rot all oranges ([[2,1,1],[0,1,1],[1,0,1]]): freshCount will not reach 0 by the end of BFS. Returns -1.
  • Single row/column grids: The bounds checking grid.length, grid[0].length, row + 1 < grid.length, etc., correctly handles these dimensions.

The solution's logic is robust against these common scenarios.

5. Learning Points

  • Similar Problems that Use this Pattern:
    • 286 - Walls and Gates (Medium): Find the distance from every empty room to the nearest gate. This is a classic multi-source BFS problem where you start BFS from all gates simultaneously.
    • 542 - 01 Matrix (Medium): For each cell, find the distance to the nearest 0. Similar to Walls and Gates, it's a multi-source BFS starting from all 0s.
    • 1091 - Shortest Path in Binary Matrix (Medium): Find the shortest clear path in a binary matrix. A single-source BFS, often involving 8-directional movement.
  • Common Mistakes with this Pattern:
    • Forgetting levelSize for time tracking: A very common mistake is to increment minutes inside the inner loop or simply after each queue.shift(). This leads to incorrect time calculation because it doesn't account for all oranges rotting simultaneously in one minute. The levelSize snapshot is crucial.
    • Incorrect Bounds Checking: As observed in the POST_MORTEM.md, bounds checking (e.g., newRow >= 0 && newRow < grid.length) can be tricky. Always remember to check the new coordinates before accessing grid[newRow][newCol].
    • Re-processing Cells: Not marking cells as visited/rotted (grid[newRow][newCol] = 2) after adding them to the queue can lead to infinite loops or incorrect freshCount if the same orange is added multiple times.
    • Off-by-one in minutes count: Incrementing minutes even if the last level processed added no new oranges (i.e., queue.length is 0 after processing levelSize items) can lead to an extra minute. The if (queue.length > 0) { minutes++; } condition correctly prevents this.
    • Not handling the "impossible" case: Forgetting to check freshCount > 0 at the very end to return -1.
  • Variations of this Problem:
    • Different grid dimensions or non-rectangular grids.
    • Different adjacency rules (e.g., 8-directional movement, knight's move).
    • Obstacles (cells that cannot be traversed or rotted).
    • Weighted 'rotting' times (e.g., some oranges take longer to rot), which would likely require Dijkstra's algorithm.

6. Code Quality

The code quality is good, demonstrating clarity and adherence to standard practices.

  • Variable Naming: Variables like freshCount, minutes, levelSize, row, and col are clear and descriptive, making the code easy to understand.
  • Code Structure: The initialization loop is separate from the BFS loop, which is clean. The BFS loop correctly uses levelSize to manage levels, which is a best practice for problems requiring time/level tracking.
  • Readability: The explicit if statements for checking each of the four neighbors, while slightly verbose, are very clear, especially for someone learning grid traversal and bounds checking. Given the POST_MORTEM.md mentioned difficulty with bounds checking, this explicit approach is beneficial for clarity.
  • TypeScript Best Practices:
    • Uses let for mutable variables and const implicitly for levelSize within the loop scope.
    • Type annotations (number[][], number) are used.
    • Array destructuring (const [row, col] = queue.shift()!) is modern and clean.
    • The non-null assertion operator ! on queue.shift() is appropriate here because the while (queue.length > 0) condition guarantees an element will be present.

Suggestions for Improvement (Minor/Alternative Style):

  • Consolidate Neighbor Checks (Alternative): While the current explicit checks are clear, for more complex grid problems or if you prefer conciseness, you can use deltaRow and deltaCol arrays to iterate through neighbors. This reduces repetition and can be easier to adapt for different directional movements (e.g., 8-directional).

    const directions = [
      [-1, 0], // up
      [1, 0],  // down
      [0, -1], // left
      [0, 1]   // right
    ];
    
    // Inside the BFS loop, after const [row, col] = queue.shift()!;
    for (const [dr, dc] of directions) {
      const newRow = row + dr;
      const newCol = col + dc;
    
      // Check bounds and if it's a fresh orange
      if (newRow >= 0 && newRow < grid.length &&
          newCol >= 0 && newCol < grid[0].length &&
          grid[newRow][newCol] === 1) {
        queue.push([newRow, newCol]);
        grid[newRow][newCol] = 2; // Mark as rotten
        freshCount--;
      }
    }

    This is a stylistic choice; the current approach is perfectly acceptable and arguably more explicit for someone learning.

7. Alternative Approaches

  1. DFS (Depth-First Search) for each fresh orange: One could conceptually try to use DFS to find the minimum time for each fresh orange to rot. However, DFS is generally not suited for finding shortest paths in unweighted graphs because it explores depth-first, meaning it might find a long path before a shorter one. This approach would be significantly more complex and likely less efficient, possibly leading to redundant computations or TLE (Time Limit Exceeded) without heavy memoization or a complex state-tracking mechanism.
  2. Iterative Grid Simulation: This approach would involve repeatedly scanning the entire grid. In each "minute" (iteration), you identify all fresh oranges adjacent to a rotten orange from the previous minute. You then mark these identified fresh oranges as rotten for the current minute. This requires careful handling (e.g., using a temporary grid or a different state for "rotting this minute") to avoid oranges rotting prematurely within the same minute. This essentially simulates the level-by-level aspect of BFS but without an explicit queue, potentially leading to higher constant factors in performance due to repeated full grid scans per minute. Its overall complexity would be similar to BFS (O(m*n*max_minutes)), but BFS is generally more elegant and efficient for managing the "frontier" of rotting oranges.

The BFS approach implemented is the most idiomatic and efficient solution for this problem.

@pertrai1

Copy link
Copy Markdown
Owner Author

🎯 How This Solution Works

Think of this problem like a zombie outbreak in a city grid. Some oranges start out "infected" (rotten), and every minute, the infection spreads to their 4 immediate neighbors (up, down, left, right). We need to figure out how long until every healthy orange becomes infected—or if some oranges are in isolated pockets that the infection can never reach.

The Setup (Lines 7-16)

Before the outbreak begins, we take a census:

  • Count the healthy oranges (freshCount++) — these are our targets
  • Identify patient zero(s) — all initially rotten oranges go into a queue

The queue is our "infection frontier" — it contains all the zombies ready to spread.

The Outbreak (Lines 18-51)

Here's where it gets interesting. BFS processes the infection in waves:

Wave 1 (Minute 0): All initially rotten oranges infect their neighbors
Wave 2 (Minute 1): The newly-infected oranges from Wave 1 infect their neighbors
Wave 3 (Minute 2): And so on...

The critical trick is on line 20: `const levelSize = queue.length`

This is like taking a "snapshot" of how many zombies exist right now. We process exactly that many, and all the new zombies they create form the next wave. Without this snapshot, new zombies would immediately start infecting others in the same minute, breaking the wave structure.

The Mechanics

For each zombie orange at position (row, col):

  1. Check north: Is there a healthy orange above me? Infect it, mark it rotten, decrement the healthy count
  2. Check south: Same for below
  3. Check east: Same for right
  4. Check west: Same for left

Why mark immediately (lines 28, 33, 38, 43)? Imagine an orange at the center of a cross surrounded by 4 rotten oranges. Without marking, it would get added to the queue 4 times! By marking it the instant we add it, we ensure "once infected, always infected."

The Verdict (Line 53)

After all waves complete:

  • If freshCount > 0, some oranges were unreachable (isolated pockets) → return -1
  • Otherwise, all oranges rotted → return the number of minutes elapsed

Why This Works

BFS guarantees the minimum time because:

  • It explores by distance: all oranges 1 step away, then 2 steps away, then 3, etc.
  • Each "level" of BFS corresponds exactly to one minute
  • The first time we reach an orange is the fastest possible time to reach it

Think of it like ripples spreading in water—each ripple is one minute, and the ripples always expand outward uniformly. That's BFS in action! 🌊

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new LeetCode Medium solution (#994 Rotting Oranges) to the learning repository, including documentation and a Vitest test suite, and indexes the problem in the global problems list.

Changes:

  • Added multi-source BFS TypeScript solution for Rotting Oranges.
  • Added Vitest tests for core scenarios and edge cases.
  • Added problem README + post-mortem notes and updated docs/PROBLEMS.md index.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
leetcode/medium/0994-rotting-oranges/rotting-oranges.ts Implements multi-source BFS to compute minimum rotting time.
leetcode/medium/0994-rotting-oranges/rotting-oranges.test.ts Adds Vitest coverage for common scenarios and failure case(s).
leetcode/medium/0994-rotting-oranges/README.md Documents problem statement, examples, and constraints.
leetcode/medium/0994-rotting-oranges/POST_MORTEM.md Adds guided TDD debrief/retrospective notes for the session.
docs/PROBLEMS.md Adds #994 to the difficulty list and BFS/DFS pattern index.

Comment on lines +20 to +24
const levelSize = queue.length;

for (let i = 0; i < levelSize; i++) {
const [row, col] = queue.shift()!;

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using queue.shift() in the BFS loop makes each dequeue O(n) due to array reindexing, which can inflate the runtime for larger grids. Prefer an index-based queue (e.g., let head = 0; and read queue[head++]) to keep dequeues O(1) (see similar approach in leetcode/medium/1229-shortest-path-with-alternating-colors/shortest-path-with-alternating-colors.ts).

Copilot uses AI. Check for mistakes.
Comment thread leetcode/medium/0994-rotting-oranges/rotting-oranges.ts Outdated
Comment on lines +10 to +13
it('should return 0 when there are only rotten oranges', () => {
const grid = [[2]];
expect(orangesRotting(grid)).toBe(0);
});

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite doesn’t currently cover the edge case where there are fresh oranges but no initially rotten oranges (e.g., [[1]]), which should return -1. Adding a dedicated test for this scenario would exercise the “queue starts empty” path and guard against regressions in the initialization logic.

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Feb 11, 2026

Copy link
Copy Markdown

Claude finished @pertrai1's task in 2m 5s —— View job


📋 Review Checklist

  • Read all changed files
  • Verify algorithmic correctness
  • Validate complexity analysis
  • Assess code quality
  • Provide pattern identification and learning insights
  • Suggest related problems

✅ Correctness Review

The solution is algorithmically correct and handles all required cases properly:

Edge Cases Handled:

  • Empty grid / no fresh oranges (returns 0)
  • All oranges already rotten (returns 0)
  • Impossible to rot all oranges (returns -1)
  • Multi-source BFS starting from multiple rotten oranges

Test Coverage:
The test suite covers all critical scenarios. Mental trace through Example 1 from README:

  • Grid: [[2,1,1],[1,1,0],[0,1,1]]
  • Initial state: 6 fresh oranges, queue has [[0,0]] (one rotten at position [0,0])
  • Minute 0 (level 0): levelSize=1, process [0,0], rots [0,1] and [1,0] → freshCount=4, queue=[[0,1],[1,0]]
  • Minute 1 (level 1): levelSize=2, process [0,1] (rots [0,2]), process [1,0] (rots [1,1], [2,0]) → freshCount=1, queue=[[0,2],[1,1],[2,0]]
  • Minute 2 (level 2): levelSize=3, process [0,2], [1,1] (rots [2,1]), [2,0] (already processed) → freshCount=0, queue=[[2,1]]
  • Minute 3 (level 3): levelSize=1, process [2,1] (rots [2,2]) → freshCount=0 (wait, should be -1 after decrement), queue=[[2,2]]
  • Minute 4 (level 4): levelSize=1, process [2,2] (no fresh neighbors) → queue empty
  • Result: freshCount=0, minutes=4 ✓

Wait, let me recount. Initial grid has:

  • Row 0: [2,1,1] → 1 rotten, 2 fresh
  • Row 1: [1,1,0] → 2 fresh
  • Row 2: [0,1,1] → 2 fresh
  • Total fresh: 6

Actually, looking more carefully at the grid initialization in the test (rotting-oranges.test.ts:16-20), let me re-trace:

  • Position [0,0]=2 (rotten), [0,1]=1 (fresh), [0,2]=1 (fresh)
  • Position [1,0]=1 (fresh), [1,1]=1 (fresh), [1,2]=0 (empty)
  • Position [2,0]=0 (empty), [2,1]=1 (fresh), [2,2]=1 (fresh)
  • Fresh count: 5 ([0,1], [0,2], [1,0], [1,1], [2,1], [2,2]) - wait that's 6. Let me count cells:
    • Row 0: 2 fresh ([0,1], [0,2])
    • Row 1: 2 fresh ([1,0], [1,1])
    • Row 2: 2 fresh ([2,1], [2,2])
    • Total: 6 fresh oranges

The solution correctly returns 4 minutes. ✓


📊 Complexity Analysis

Time Complexity: O(m × n) ✅ Accurate

  • Each cell is visited at most once (added to queue once, marked rotten once)
  • Initialization loop (lines 8-16): O(m × n) to scan entire grid
  • BFS processes each cell at most once: O(m × n)
    • Each cell can only be added to the queue once because it's marked rotten immediately (lines 28, 33, 38, 43)
    • Checking 4 neighbors per cell is O(1) per cell
  • Total: O(m × n)

Space Complexity: O(m × n) ✅ Accurate

  • Worst case: All cells are initially rotten → entire grid added to queue at once
  • Typical case: Only rotten oranges and their spreading neighbors in queue
  • Grid is modified in-place (no extra space for visited tracking)
  • Queue stores coordinate pairs [row, col], worst case O(m × n)

The complexity analysis in the PR description is 100% correct.


🎯 Pattern Recognition

Pattern: Multi-Source Breadth-First Search (BFS)

Why This Pattern Works:

This is a classic "shortest distance from multiple sources" problem. Multi-source BFS is the perfect pattern because:

  1. BFS guarantees shortest path — processes nodes in increasing distance from source(s)
  2. Level-by-level processing maps to time — each complete BFS level represents exactly 1 minute elapsed
  3. Multiple starting points — all initially rotten oranges are sources that rot simultaneously
  4. 4-directional propagation — natural fit for BFS neighbor exploration in grids

Key Insight (The "Aha!" Moment):

The critical technique is snapshotting the queue size before processing each level (rotting-oranges.ts:20):

const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) { ... }

Why this works: Without the snapshot, newly-added oranges from the current level would be processed immediately within the same loop iteration, breaking the invariant that "each complete BFS level = exactly 1 minute". The snapshot ensures all oranges at distance D rot before any at distance D+1.

This is beautifully explained in the author's own comment on the PR and in the POST_MORTEM.md.


💎 Code Quality

Strengths:

Excellent variable namingfreshCount, levelSize, queue, row, col are all clear and semantic
Clean structure — Three distinct phases (initialization, BFS, return) with clear comments
Proper bounds checking — All four directions check the new position before accessing the grid
Efficient state tracking — Uses grid modification (grid[row][col] = 2) to mark visited, avoiding a separate visited set
Correct minute counting — Only increments minutes if queue has remaining elements (line 48-50), preventing an off-by-one error
TypeScript best practices:

  • Uses const for immutable values, let for mutable variables
  • Type annotation [number, number][] for queue is precise (shows coordinate pairs)
  • Array destructuring const [row, col] = queue.shift()! is clean and modern
  • Non-null assertion ! is appropriate given the queue.length > 0 guard

Minor Observations (Not Issues):

  1. Queue implementation note: Using queue.shift() is O(n) per operation due to array reindexing. For the constraints of this problem (max 10×10 grid), this is completely fine. For larger grids, an index-based approach would be more efficient:

    let head = 0;
    while (head < queue.length) {
      const [row, col] = queue[head++];
      // ...
    }

    Trade-off: The current approach is more readable and idiomatic. For this problem size, readability wins. ✅

  2. Directions array alternative: The explicit 4-directional checks (lines 26-45) are very clear and easy to debug. An alternative using a directions array would be more concise:

    const directions = [[1,0], [-1,0], [0,1], [0,-1]];
    for (const [dr, dc] of directions) {
      const newRow = row + dr, newCol = col + dc;
      if (newRow >= 0 && newRow < grid.length && 
          newCol >= 0 && newCol < grid[0].length && 
          grid[newRow][newCol] === 1) {
        queue.push([newRow, newCol]);
        grid[newRow][newCol] = 2;
        freshCount--;
      }
    }

    Trade-off: The current explicit approach is more beginner-friendly and easier to debug, especially given the POST_MORTEM.md mentions bounds checking was challenging. Both are equally valid. ✅


🎓 Learning Summary

What Makes This Solution Elegant

  1. Level-by-level invariant — The snapshot pattern (line 20) elegantly guarantees each complete level = 1 minute
  2. In-place modification — Marking oranges rotten in the grid saves space and prevents duplicate processing
  3. Fresh count optimization — Tracking freshCount allows O(1) final check instead of re-scanning the grid
  4. Clean separation of concerns — Initialization, BFS processing, and result validation are clearly separated

Common Mistakes with This Pattern

Forgetting to snapshot queue size:

// WRONG: Processes newly-added oranges immediately
while (queue.length > 0) {
  const [row, col] = queue.shift()!;
  // ... adds neighbors to queue during iteration
  minutes++; // ❌ Increments per orange, not per level
}

Not marking oranges as rotten when adding to queue:

// WRONG: Same orange added multiple times
if (grid[newRow][newCol] === 1) {
  queue.push([newRow, newCol]);
  // ❌ Missing: grid[newRow][newCol] = 2
}
// Result: If an orange has multiple rotten neighbors, 
// it gets added once per neighbor → duplicate processing

Incrementing minutes unconditionally:

// WRONG: Adds extra minute when queue becomes empty
while (queue.length > 0) {
  // ... process level
  minutes++; // ❌ Should check if queue.length > 0 first
}

Checking current position instead of new position in bounds:

// WRONG: Checks current row instead of new row
if (row >= 0 && row < grid.length && grid[row + 1][col] === 1) {
  // ❌ Should check: row + 1 < grid.length
}

Your solution avoids all of these pitfalls. ✅


🔗 Related Problems to Practice

Same Pattern (Multi-Source BFS):

  1. 286 - Walls and Gates (Medium) 🔒
    Start from all gates (value 0) and fill distances to empty rooms using multi-source BFS. Nearly identical pattern to Rotting Oranges.

  2. 542 - 01 Matrix (Medium)
    Find nearest 0 for each cell. Multi-source BFS starting from all 0s. Same "distance from multiple sources" concept.

  3. 1162 - As Far from Land as Possible (Medium)
    Find water cell farthest from land. Multi-source BFS from all land cells, track maximum distance.

Level-by-Level BFS Practice:

  1. 1091 - Shortest Path in Binary Matrix (Medium)
    Single-source BFS with 8-directional movement. Reinforces level tracking and bounds checking.

  2. 1926 - Nearest Exit from Entrance in Maze (Medium)
    Single-source BFS to find shortest path to any exit. Practices level-by-level distance tracking.

Why These Help:

  • Walls and Gates (#286) — Nearly identical to Rotting Oranges, perfect for muscle memory
  • 01 Matrix (#542) — Same pattern, different domain (reinforces multi-source intuition)
  • Shortest Path in Binary Matrix (#1091) — Single-source variant, practices bounds checking with 8 directions

🎯 Final Assessment

Overall: This is an exemplary TDD-driven solution with excellent code quality, correct complexity analysis, and comprehensive test coverage.

Highlights:

  • ✅ Clean, readable implementation with semantic naming
  • ✅ Correct algorithmic approach (multi-source BFS)
  • ✅ Accurate complexity analysis (O(m × n) time and space)
  • ✅ All edge cases covered in tests
  • ✅ Excellent POST_MORTEM.md documenting the learning journey
  • ✅ Self-review questions in PR description demonstrate deep understanding
  • ✅ Educational inline comments on the PR showing mastery of the pattern

Learning Value: The level-by-level BFS pattern you've mastered here is foundational for dozens of graph/grid problems. The snapshot technique (line 20) is a game-changer for any "time-based propagation" or "distance-based spreading" problem.

One Minor Test Gap: The test suite doesn't cover Example 3 from the README ([[0,2]] → 0) or the edge case where there are only fresh oranges with no rotten oranges ([[1]] → -1). Consider adding:

it('should return 0 when grid has no fresh oranges (Example 3)', () => {
  const grid = [[0, 2]];
  expect(orangesRotting(grid)).toBe(0);
});

it('should return -1 when only fresh oranges exist with no rotten ones', () => {
  const grid = [[1, 1], [1, 1]];
  expect(orangesRotting(grid)).toBe(-1);
});

However, the core logic handles these cases correctly based on code inspection.

Recommendation:Approve and merge

Great work on this solution! The guided TDD approach clearly helped build a robust, well-tested implementation with deep understanding of the underlying pattern. The POST_MORTEM.md is particularly valuable for documenting the learning journey. 🎉


@github-actions

Copy link
Copy Markdown

🤖 Gemini Code Review (Model: gemini-2.5-flash)

Here's a detailed review of the Rotting Oranges solution:


1. Pattern Identification

This solution uses the Multi-Source Breadth-First Search (BFS) algorithmic pattern. It's a classic graph traversal algorithm, specifically applied to a grid, where the "nodes" are grid cells and "edges" are 4-directional adjacencies. The "multi-source" aspect comes from starting the BFS simultaneously from all initially rotten oranges.

2. Complexity Verification

  • Time Complexity: O(m * n)

    • Verification: The stated time complexity is accurate.
    • Explanation:
      1. The initial loop iterates through all m * n cells of the grid once to count fresh oranges and enqueue initially rotten ones. This takes O(m * n) time.
      2. The BFS while loop processes each cell (orange) at most once. When a fresh orange becomes rotten, it's added to the queue, and its state in the grid is updated to 2 (rotten). This prevents it from being added to the queue again or being processed as fresh. For each cell processed, its 4 neighbors are checked.
      3. Therefore, the total time spent in the BFS part is proportional to the number of cells and edges (which are fixed for a grid of m*n cells, 4 edges per cell). This leads to an overall O(m * n) time complexity.
  • Space Complexity: O(m * n)

    • Verification: The stated space complexity is accurate.
    • Explanation:
      1. The queue stores the coordinates of rotten oranges. In the worst-case scenario (e.g., all oranges are initially rotten, or all fresh oranges rot almost simultaneously), the queue could hold up to O(m * n) elements.
      2. The solution modifies the input grid in-place to mark oranges as rotten. If in-place modification were not allowed, an additional visited array of O(m * n) would be required.
      3. Thus, the dominant space factor is the queue, leading to O(m * n) space complexity.

3. Key Insight

The key insight for this problem is recognizing that it's a shortest path problem on an unweighted graph, where "shortest" means the minimum number of minutes. BFS is inherently suited for this because it explores the graph level by level. Each "level" in the BFS corresponds to one minute of elapsed time.

The "aha!" moment is understanding how to correctly track time in a multi-source BFS:

  1. Start all BFS traversals simultaneously: Initialize the queue with all rotten oranges present at minute 0.
  2. Process level by level: Use a levelSize (or similar mechanism) to process all oranges that rot at the current minute before incrementing the minutes counter. This ensures that all oranges at distance D from an initial rotten orange are processed before any oranges at distance D+1, accurately reflecting the minute-by-minute rotting process.
  3. Use the grid as a visited set: By changing grid[row][col] from 1 to 2 when a fresh orange rots, you effectively mark it as visited and prevent it from being re-added to the queue or re-processed, ensuring correctness and efficiency.

4. Edge Case Analysis

The current solution handles the following edge cases correctly:

  • No fresh oranges initially:
    • grid = [[0,2]] (Example 3): freshCount will be 0. queue will contain [[0,1]]. The BFS will run, but minutes will only increment if queue.length > 0 after processing a level. Since no fresh oranges are found, queue.length will become 0 at the end of the first level, and minutes will remain 0. Correctly returns 0.
    • grid = [[0,0]]: freshCount will be 0. queue will be empty. The while loop won't run. Correctly returns 0.
    • grid = [[2]]: freshCount will be 0. queue will contain [[0,0]]. Similar to [[0,2]], minutes remains 0. Correctly returns 0.
  • No rotten oranges initially, but fresh oranges exist:
    • grid = [[1,1],[1,0]]: freshCount will be 3. queue will be empty. The while loop won't run. The final check freshCount > 0 returns -1. Correctly returns -1.
  • Impossible to rot all oranges (disconnected components):
    • grid = [[2,1,1],[0,1,1],[1,0,1]] (Example 2): The orange at [2,0] is fresh and isolated. The BFS will run, freshCount will decrement for reachable oranges, but freshCount for [2,0] will remain 1. After the while loop, freshCount > 0 will be true. Correctly returns -1.
  • Single-cell grid: Handled by the bounds checks (e.g., grid = [[1]] returns -1, grid = [[2]] returns 0).
  • Grid with only empty cells: grid = [[0,0]]: freshCount is 0, queue is empty. Returns 0. Correct.

The solution appears robust for all standard edge cases.

5. Learning Points

  • Similar Problems using this pattern:
  • Common Mistakes with this pattern:
    • Incorrectly handling multiple sources: Trying to run a separate BFS for each initial rotten orange instead of enqueuing all of them at the start.
    • Not tracking visited cells: This can lead to infinite loops or incorrect path lengths if cells are re-added to the queue. Modifying the grid in-place (as done here) or using a separate visited array/set is crucial.
    • Incorrectly tracking time/levels: Incrementing the minutes counter for every dequeued element instead of for each complete "level" of the BFS. The levelSize approach is key for problems requiring minimum time/steps.
    • Off-by-one errors in bounds checking: Forgetting to check newRow >= 0, newRow < grid.length, etc., before accessing grid[newRow][newCol].
  • Variations of this problem:
    • Different adjacency rules (e.g., 8-directional movement like a king in chess).
    • Weighted grid cells (e.g., some oranges take longer to rot) – this would require Dijkstra's algorithm.
    • Finding the longest time for all oranges to rot (could still be BFS, but perhaps tracking maximum distance).

6. Code Quality

The code quality is generally good:

  • Variable Naming: freshCount, minutes, queue, levelSize, row, col are clear and descriptive.
  • Code Structure: The solution is well-structured, with clear initialization, a main BFS loop, and a final return statement. Comments are helpful for understanding the BFS logic.
  • Readability: The code is easy to follow.
  • TypeScript Best Practices:
    • queue: [number, number][] explicitly types the queue elements, which is good.
    • Using ! (non-null assertion operator) on queue.shift()! is acceptable in this context because the while (queue.length > 0) condition guarantees shift() will not return undefined.
    • const is used for queue (though its contents change, the reference itself is constant) and levelSize. let is correctly used for freshCount and minutes as they are reassigned.

Minor Improvements:

  • Extract dr and dc arrays for neighbor directions: This improves readability and reduces repetition, especially if the number of directions changes (e.g., 8-directional).

    export function orangesRotting(grid: number[][]): number {
      const rows = grid.length;
      const cols = grid[0].length; // Get cols from first row, assuming non-empty grid
      let freshCount: number = 0;
      let minutes = 0;
    
      const queue: [number, number][] = [];
    
      // Define direction vectors for (up, down, left, right)
      const dr = [-1, 1, 0, 0];
      const dc = [0, 0, -1, 1];
    
      // Initialize queue and count fresh oranges
      for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
          if (grid[r][c] === 2) {
            queue.push([r, c]);
          } else if (grid[r][c] === 1) {
            freshCount++;
          }
        }
      }
    
      // Handle edge case: no fresh oranges initially
      if (freshCount === 0) {
        return 0;
      }
    
      // BFS
      while (queue.length > 0) {
        const levelSize = queue.length;
        let newOrangesRottenThisMinute = false; // Flag to check if any oranges rotted in this minute
    
        for (let i = 0; i < levelSize; i++) {
          const [r, c] = queue.shift()!;
    
          // Check all 4 neighbors
          for (let j = 0; j < 4; j++) {
            const newR = r + dr[j];
            const newC = c + dc[j];
    
            // Check bounds and if neighbor is a fresh orange
            if (newR >= 0 && newR < rows && newC >= 0 && newC < cols && grid[newR][newC] === 1) {
              grid[newR][newC] = 2; // Mark as rotten
              freshCount--;
              queue.push([newR, newC]);
              newOrangesRottenThisMinute = true;
            }
          }
        }
    
        // Increment minutes only if oranges rotted in this minute AND there are more oranges to process
        // The original `if (queue.length > 0)` here implicitly covers `newOrangesRottenThisMinute`
        // but adding the flag can make it more explicit.
        // The original logic `if (queue.length > 0) { minutes++; }` is actually more robust
        // for the final minute increment, as it correctly increments minutes only if the next level is not empty.
        // Let's stick to the original logic for minute increment, but use dr/dc.
        if (queue.length > 0) { // If there are still oranges in the queue, it means a new minute has passed
          minutes++;
        }
      }
    
      return freshCount > 0 ? -1 : minutes;
    }

    Self-correction: The original if (queue.length > 0) { minutes++; } at the end of the while loop is actually quite elegant and robust. It correctly handles the case where the last batch of fresh oranges rots, but no new fresh oranges are found, ensuring minutes isn't unnecessarily incremented. My suggestion to add newOrangesRottenThisMinute flag is not strictly necessary and the existing logic is fine. The main benefit is dr/dc arrays.

  • Clarity on grid[0].length: It's good practice to store grid.length and grid[0].length in const variables (rows, cols) at the beginning for clarity and potential minor performance gain (avoiding repeated property lookups, though compilers often optimize this).

7. Alternative Approaches

  1. Iterative Simulation (Brute Force):

    • Approach: Instead of using a queue, you could simulate the rotting process minute by minute. In each minute, iterate through the entire grid. If you find a fresh orange adjacent to a rotten one, mark it as "will rot in the next minute" (e.g., 3). After scanning the entire grid, convert all 3s to 2s and increment the minute counter. Repeat until no more fresh oranges can rot or no fresh oranges remain.
    • Trade-offs:
      • Time Complexity: O(minutes * m * n). In the worst case, minutes could be O(m * n), making this O((m*n)^2), which is significantly slower than BFS's O(m * n).
      • Space Complexity: O(1) (if modifying in-place) or O(m * n) for a temporary grid to store next-minute states.
      • Pros: Conceptually simpler to implement for some, no explicit queue management.
      • Cons: Much less efficient for larger grids due to repeated full grid traversals.
  2. Depth-First Search (DFS):

    • Approach: DFS is generally not suitable for finding the shortest path or minimum time in unweighted graphs because it explores depth-first. You could adapt it by keeping track of the "time" it takes for an orange to rot, but it would require more complex state management (e.g., minTime[r][c]) and potentially re-visiting cells multiple times, making it less efficient than BFS for this specific problem.
    • Trade-offs:
      • Time Complexity: Can be O(m * n) if carefully implemented with memoization/visited array to avoid redundant computations, but often harder to guarantee shortest path.
      • Space Complexity: O(m * n) in the worst case for the recursion stack depth.
      • Pros: Can be more concise for certain graph problems like path existence or connected components.
      • Cons: Not naturally suited for shortest path on unweighted graphs, typically requires more effort to ensure correctness and efficiency compared to BFS for this type of problem.

For "Rotting Oranges," the Multi-Source BFS approach is unequivocally the most optimal and idiomatic solution.

@pertrai1 pertrai1 merged commit 3dc18ed into main Feb 11, 2026
3 of 4 checks passed
@pertrai1 pertrai1 deleted the tdd/0994-rotting-oranges branch February 11, 2026 01:54
@github-actions

Copy link
Copy Markdown

📅 Spaced Repetition Reviews Scheduled!

Great job solving #0994 - Rotting Oranges! 🎉

To help you retain this knowledge long-term, I've scheduled 5 review sessions using spaced repetition:

Review Interval Schedule Logic
1st Review 1 day after solving Scheduled now
2nd Review 3 days after 1st review Auto-scheduled when 1st completes
3rd Review 7 days after 2nd review Auto-scheduled when 2nd completes
4th Review 14 days after 3rd review Auto-scheduled when 3rd completes
5th Review 30 days after 4th review Auto-scheduled when 4th completes

What to expect:

  • Your 1st review is scheduled for tomorrow
  • Each subsequent review is scheduled automatically when you complete the previous one
  • This ensures proper spacing even if you complete a review a few days late
  • GitHub issues will be created automatically for each review
  • Each issue will link back to your solution

🧠 Why Spaced Repetition?

Research shows that reviewing material at increasing intervals dramatically improves retention. This adaptive scheduling ensures optimal spacing based on when you actually complete each review!

Check docs/reviews/review-schedule.json to see your review schedule.

github-actions Bot pushed a commit that referenced this pull request Feb 11, 2026
  Problem: #0994 - Rotting Oranges
  PR: #231
  First review scheduled (subsequent reviews auto-scheduled on completion)

  [skip ci]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code challenge this is a coding challenge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants