learn: solve Rotting Oranges (#994) via guided TDD#231
Conversation
|
💡 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. |
|
Claude finished @pertrai1's task in 1m 27s —— View job 📋 Review Checklist
✅ Correctness ReviewThe solution is algorithmically correct and handles all required cases properly: ✅ Edge Cases Handled:
✅ Test Coverage:
📊 Complexity AnalysisTime Complexity: O(m × n) ✅ Accurate
Space Complexity: O(m × n) ✅ Accurate
The author's complexity analysis in the PR description is 100% correct. 🎯 Pattern RecognitionPattern: Multi-Source Breadth-First Search (BFS) Why This Pattern Works:
Key Insight (The "Aha!" Moment): 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 QualityStrengths:
Minor Suggestions (Not Issues):
🎓 Learning SummaryWhat Makes This Solution Elegant
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 PracticeSame Pattern (Multi-Source BFS):
Level-by-Level BFS Practice:
Why These Help:
🎯 Final AssessmentOverall: This is an exemplary TDD-driven solution with excellent code quality, correct complexity analysis, and comprehensive test coverage. Highlights:
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. 🎉 |
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughAdds 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
|
💡 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:
Common mistake: Checking the current position ( |
|
💡 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 immediatelyWhy 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:
The pattern: In BFS/DFS on grids, mark cells as visited when adding to queue, not when processing. This prevents duplicates. |
✅ Test ResultsTests have been executed for this pull request. 📊 Coverage Summary
Check the workflow logs for detailed test output. |
|
🤖 Gemini Code Review (Model: Here's a detailed review of the 1. Pattern IdentificationThis solution uses a Multi-Source Breadth-First Search (BFS).
2. Complexity Verification
3. Key InsightThe 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:
4. Edge Case AnalysisThe current solution handles the following edge cases correctly:
The solution appears robust for the given constraints. 5. Learning Points
6. Code QualityThe code quality is excellent:
Minor Improvement Suggestion (for conciseness and flexibility): Instead of four separate 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 The original 7. Alternative Approaches
|
|
🤖 Gemini Code Review (Model: Here's a review of the 1. Pattern IdentificationThis solution uses Multi-Source Breadth-First Search (BFS).
2. Complexity Verification
3. Key InsightThe key insight for solving Rotting Oranges efficiently is to treat each "minute" as a level in a BFS traversal.
This level-by-level processing ensures that the 4. Edge Case AnalysisThe current solution handles the following critical edge cases correctly:
The solution's logic is robust against these common scenarios. 5. Learning Points
6. Code QualityThe code quality is good, demonstrating clarity and adherence to standard practices.
Suggestions for Improvement (Minor/Alternative Style):
7. Alternative Approaches
The BFS approach implemented is the most idiomatic and efficient solution for this problem. |
🎯 How This Solution WorksThink 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:
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 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 MechanicsFor each zombie orange at position (row, col):
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:
Why This WorksBFS guarantees the minimum time because:
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! 🌊 |
There was a problem hiding this comment.
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.mdindex.
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. |
| const levelSize = queue.length; | ||
|
|
||
| for (let i = 0; i < levelSize; i++) { | ||
| const [row, col] = queue.shift()!; | ||
|
|
There was a problem hiding this comment.
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).
| it('should return 0 when there are only rotten oranges', () => { | ||
| const grid = [[2]]; | ||
| expect(orangesRotting(grid)).toBe(0); | ||
| }); |
There was a problem hiding this comment.
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.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
Claude finished @pertrai1's task in 2m 5s —— View job 📋 Review Checklist
✅ Correctness ReviewThe solution is algorithmically correct and handles all required cases properly: ✅ Edge Cases Handled:
✅ Test Coverage:
Wait, let me recount. Initial grid has:
Actually, looking more carefully at the grid initialization in the test (rotting-oranges.test.ts:16-20), let me re-trace:
The solution correctly returns 4 minutes. ✓ 📊 Complexity AnalysisTime Complexity: O(m × n) ✅ Accurate
Space Complexity: O(m × n) ✅ Accurate
The complexity analysis in the PR description is 100% correct. 🎯 Pattern RecognitionPattern: 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:
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 QualityStrengths: ✅ Excellent variable naming —
Minor Observations (Not Issues):
🎓 Learning SummaryWhat Makes This Solution Elegant
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 PracticeSame Pattern (Multi-Source BFS):
Level-by-Level BFS Practice:
Why These Help:
🎯 Final AssessmentOverall: This is an exemplary TDD-driven solution with excellent code quality, correct complexity analysis, and comprehensive test coverage. Highlights:
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 ( 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. 🎉 |
|
🤖 Gemini Code Review (Model: Here's a detailed review of the 1. Pattern IdentificationThis 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
3. Key InsightThe 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:
4. Edge Case AnalysisThe current solution handles the following edge cases correctly:
The solution appears robust for all standard edge cases. 5. Learning Points
6. Code QualityThe code quality is generally good:
Minor Improvements:
7. Alternative Approaches
For "Rotting Oranges," the Multi-Source BFS approach is unequivocally the most optimal and idiomatic solution. |
📅 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:
What to expect:
🧠 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 |
Problem: #0994 - Rotting Oranges PR: #231 First review scheduled (subsequent reviews auto-scheduled on completion) [skip ci]
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
Pattern
Multi-Source Breadth-First Search (BFS)
Learning Summary
🧠 Self-Review Questions
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.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 statementrotting-oranges.ts— Solutionrotting-oranges.test.ts— Test suitePOST_MORTEM.md— Interview debrief and retrospectiveSummary by CodeRabbit
New Features
Documentation
Tests