Skip to content

Commit 86d1c7a

Browse files
committed
feat(algorithms, matrix): rotting oranges
1 parent c0c14fc commit 86d1c7a

4 files changed

Lines changed: 209 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Rotting Oranges
2+
3+
You are given an m x n grid where each cell can have one of three values:
4+
5+
- 0 representing an empty cell,
6+
- 1 representing a fresh orange, or
7+
- 2 representing a rotten orange.
8+
9+
Every minute, any fresh orange that is 4-directionally adjacent to a rotten orange becomes rotten.
10+
11+
Return the minimum number of minutes that must elapse until no cell has a fresh orange. If this is impossible, return -1.
12+
13+
## Examples
14+
15+
![Example 1](./images/examples/rotting_oranges_example_1.png)
16+
17+
Example 1
18+
```text
19+
Input: grid = [[2,1,1],[1,1,0],[0,1,1]]
20+
Output: 4
21+
```
22+
23+
Example 2:
24+
```text
25+
Input: grid = [[2,1,1],[0,1,1],[1,0,1]]
26+
Output: -1
27+
Explanation: The orange in the bottom left corner (row 2, column 0) is never rotten, because rotting only happens 4-directionally.
28+
```
29+
30+
Example 3:
31+
```text
32+
Input: grid = [[0,2]]
33+
Output: 0
34+
Explanation: Since there are already no fresh oranges at minute 0, the answer is just 0.
35+
```
36+
37+
## Constraints
38+
39+
- m == grid.length
40+
- n == grid[i].length
41+
- 1 <= m, n <= 10
42+
- `grid[i][j]` is 0, 1, or 2.
43+
44+
## Topics
45+
46+
- Array
47+
- Breadth-First Search
48+
- Matrix
49+
50+
## Solution
51+
52+
The algorithm uses BFS (Breadth-First Search) to simulate this minute-by-minute spreading process. It starts by finding
53+
all initially rotten oranges and counting all fresh oranges. Then it processes the rotting in rounds, where each round
54+
represents one minute. In each round, all currently rotten oranges spread the rot to their adjacent fresh oranges
55+
simultaneously. The process continues until either all fresh oranges have rotted (return the number of minutes elapsed)
56+
or no more fresh oranges can be reached (return -1).
57+
58+
The key insight is to think of the rotting process as waves spreading outward from multiple sources simultaneously.
59+
Imagine dropping multiple stones into a pond at the same time - the ripples expand outward from each stone, and where
60+
they meet, they've both reached that point at the same time.
61+
62+
In our problem, the rotten oranges are like those stones, and the rotting process spreads like ripples. Each "wave" or
63+
"ripple" represents one minute of time. All oranges at distance 1 from any rotten orange will rot after 1 minute, all
64+
oranges at distance 2 will rot after 2 minutes, and so on.
65+
66+
This naturally leads us to BFS because:
67+
68+
- BFS processes nodes level by level, which perfectly models the minute-by-minute spreading
69+
- We can start with all initially rotten oranges in our queue (multiple starting points)
70+
- Each level of BFS represents one unit of time
71+
72+
The algorithm maintains a counter fresh_count for fresh oranges. As we process each level:
73+
74+
- We rot all reachable fresh oranges at the current distance
75+
- We decrement fresh_count for each newly rotten orange
76+
- We track how many levels (minutes) we've processed
77+
78+
The beauty of this approach is that BFS guarantees we're always rotting oranges in the optimal order - closest ones first.
79+
When fresh_count reaches 0, we know all fresh oranges have rotted, and the number of levels processed equals the minimum time
80+
needed.
81+
82+
If after BFS completes, fresh_count > 0, it means some fresh oranges were unreachable (isolated), so we return -1.
83+
84+
### Algorithm
85+
86+
- Initial Setup and Counting First, we traverse the entire grid once to:
87+
- Find all initially rotten oranges (value 2) and add their coordinates (i, j) to a queue `queue`
88+
- Count all fresh oranges (value 1) and store in variable `fresh_count`
89+
- This preprocessing helps us know our starting points and when to stop
90+
- BFS Initialization
91+
- Initialize `minutes_elapsed = 0` to track the number of minutes elapsed
92+
- Define directions array dirs = (-1, 0, 1, 0, -1) for 4-directional movement
93+
- Using pairwise(directions) gives us [(-1, 0), (0, 1), (1, 0), (0, -1)] representing up, right, down, left
94+
- Level-by-Level BFS Processing The main BFS loop continues while `queue` is not empty AND `fresh_count` > 0:
95+
- Increment `minutes_elapsed` at the start of each level (representing one minute passing)
96+
- Process all oranges at the current level using `for _ in range(len(queue))`:
97+
- For each rotten orange (i, j), check all 4 adjacent cells
98+
- Calculate new coordinates: x, y = i + a, j + b
99+
- If the adjacent cell is within bounds and contains a fresh orange (`grid[x][y]` == 1):
100+
- Mark it as rotten: `grid[x][y]` = 2
101+
- Add it to queue for next level: `queue.append((x, y))`
102+
- Decrement fresh count: `fresh_count -= 1`
103+
- Early termination: if `fresh_count == 0`, immediately return `minutes_elapsed`
104+
- Final Result After BFS completes:
105+
- If `fresh_count > 0`: Some fresh oranges couldn't be reached, return -1
106+
- If `fresh_count == 0`: All fresh oranges have rotted, return 0 (or `minutes_elapsed` if terminated early)
107+
108+
### Complexity Analysis
109+
110+
#### Time Complexity O(m * n)
111+
112+
The algorithm performs a BFS traversal starting from all initially rotten oranges. In the worst case, every cell in the
113+
grid needs to be visited exactly once. The initial scan to find all rotten oranges and count fresh oranges takes O(m × n)
114+
time. The BFS process visits each cell at most once, as each fresh orange becomes rotten exactly once and is added to the
115+
queue once. Therefore, the total time complexity is O(m × n).
116+
117+
#### Space Complexity O(m * n)
118+
119+
The space complexity is determined by the queue used for BFS. In the worst-case scenario, all oranges could be rotten
120+
initially (all cells contain 2), which means the queue would need to store all m × n positions at the start. Even in a
121+
more typical case where oranges rot progressively, the queue could potentially hold O(m × n) elements at any given time
122+
(for example, when a wave of rotting spreads across a large portion of the grid simultaneously). Therefore, the space
123+
complexity is O(m × n).
124+
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from typing import List, Deque, Tuple
2+
from collections import deque
3+
4+
5+
def oranges_rotting(grid: List[List[int]]) -> int:
6+
if not grid:
7+
return 0
8+
9+
# Grid dimensions
10+
rows, cols = len(grid), len(grid[0])
11+
12+
# Count fresh oranges and collect initial rotten oranges
13+
fresh_count = 0
14+
queue: Deque[Tuple[int, int]] = deque()
15+
16+
# Scan the grid to find all rotten oranges (value 2) and count fresh oranges (value 1)
17+
for row_idx in range(rows):
18+
for col_idx in range(cols):
19+
if grid[row_idx][col_idx] == 2:
20+
# Add rotten orange position to queue
21+
queue.append((row_idx, col_idx))
22+
elif grid[row_idx][col_idx] == 1:
23+
# Increment fresh oranges
24+
fresh_count += 1
25+
26+
# Time elapsed in minutes
27+
minutes_elapsed = 0
28+
# Direction vectors for 4-directional movement (up, down, left, right)
29+
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
30+
31+
# BFS to rot adjacent oranges
32+
while queue and fresh_count > 0:
33+
minutes_elapsed += 1
34+
35+
# Process all oranges that rot in the current minute
36+
current_level_size = len(queue)
37+
for _ in range(current_level_size):
38+
current_row, current_col = queue.popleft()
39+
40+
# Check all four adjacent cells
41+
for row_delta, col_delta in directions:
42+
next_row = current_row + row_delta
43+
next_col = current_col + col_delta
44+
45+
# Check if the adjacent cell is within bounds and contains a fresh orange
46+
if (
47+
0 <= next_row < rows
48+
and 0 <= next_col < cols
49+
and grid[next_row][next_col] == 1
50+
):
51+
# Rot the fresh orange
52+
grid[next_row][next_col] = 2
53+
queue.append((next_row, next_col))
54+
fresh_count -= 1
55+
56+
# Early termination if all oranges are rotten
57+
if fresh_count == 0:
58+
return minutes_elapsed
59+
60+
# Return -1 if there are still fresh oranges, otherwise return 0
61+
return -1 if fresh_count > 0 else 0
195 KB
Loading
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from utils.test_utils import custom_test_name_func
5+
from algorithms.matrix.rotting_oranges import oranges_rotting
6+
7+
ROTTING_ORANGES_TEST_CASES = [
8+
([[2, 1, 1], [1, 1, 0], [0, 1, 1]], 4),
9+
([[2, 1, 1], [0, 1, 1], [1, 0, 1]], -1),
10+
([[0, 2]], 0),
11+
([[0, 1, 2], [1, 0, 2], [0, 2, 1]], -1),
12+
([[0, 1, 1], [1, 0, 1], [0, 1, 1]], -1),
13+
]
14+
15+
16+
class RottingOrangesTestCase(unittest.TestCase):
17+
@parameterized.expand(ROTTING_ORANGES_TEST_CASES, name_func=custom_test_name_func)
18+
def test_oranges_rotting(self, grid: List[List[int]], expected: int):
19+
actual = oranges_rotting(grid)
20+
self.assertEqual(expected, actual)
21+
22+
23+
if __name__ == "__main__":
24+
unittest.main()

0 commit comments

Comments
 (0)