407. Trapping Rain Water II
Problem Description
You are given a 2D elevation map represented as an m x n
matrix called heightMap
, where each cell contains an integer value representing the height at that position. When it rains, water gets trapped in the lower areas between higher elevations. Your task is to calculate the total volume of water that can be trapped after raining.
The water trapping works similar to a real-world scenario: water fills up the lower areas and is contained by the surrounding higher walls. Water can only be trapped if it's surrounded by heights that are greater than or equal to the water level on all sides (including diagonally adjacent cells are not considered - only up, down, left, right).
Think of it like pouring water onto a 3D terrain model - the water will pool in valleys and depressions, with the boundaries of the matrix acting as edges where water can flow off. The volume of trapped water at each cell is determined by the minimum height of all possible paths to reach the boundary of the matrix.
For example:
- If a cell with height 3 is completely surrounded by cells with heights 5 or greater, and the minimum "wall" height to reach the boundary is 5, then that cell can trap 2 units of water (5 - 3 = 2).
- Cells on the boundary of the matrix cannot trap any water as water would flow off the edge.
The function should return the total volume of water that can be trapped across the entire elevation map.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The 2D grid can be viewed as a graph where each cell is a node connected to its 4 adjacent neighbors (up, down, left, right).
Is it a tree?
- No: The grid structure has cycles and multiple paths between cells, not a tree structure.
Is the problem related to directed acyclic graphs?
- No: The problem isn't about directed edges or acyclic properties.
Is the problem related to shortest paths?
- Yes: We need to find the minimum height barrier to reach the boundary from each cell, which is essentially finding the "shortest" (minimum height) path to the boundary.
Is the graph Weighted?
- Yes: Each cell has a height value that acts as a weight. We need to track the maximum height encountered along paths to the boundary.
Dijkstra's Algorithm?
- Yes: We use a modified Dijkstra's approach with a priority queue (min-heap) to process cells by their height values, starting from the boundary cells.
Conclusion: The flowchart suggests using Dijkstra's Algorithm, which is implemented here as a BFS-like approach with a priority queue. We start from all boundary cells (which can't trap water) and work inward, always processing the lowest height cells first. This ensures we correctly determine the water level that can be trapped at each interior cell based on the minimum barrier height to reach the boundary.
Intuition
Think about how water behaves in real life - it always flows from high to low areas and eventually finds its way out through the lowest possible exit point. In this 2D elevation map, water can only escape through the boundary cells, so the key insight is: the amount of water that can be trapped at any cell depends on the minimum height barrier it needs to overcome to reach the boundary.
Imagine you're standing at an interior cell and water is rising around you. The water level at your position can only rise as high as the lowest "wall" you'd need to climb over to escape to the boundary. If that minimum escape height is 5 and you're standing at height 3, then water can fill up to level 5, trapping 2 units of water above you.
To find this minimum escape height for each cell, we can think of it like flood-filling from the outside inward. We start from all boundary cells simultaneously (since water can escape from any boundary) and gradually move inward, always processing the lowest height cells first. This is crucial because:
-
Water enters from the lowest points first: Just like pouring water into a container, it will overflow at the lowest rim first. By using a min-heap to always process the cell with minimum height, we simulate this natural water flow.
-
The water level is determined by the path of least resistance: When we visit a new cell from a processed cell, the water level at the new cell is at least as high as the maximum we've seen so far on the path from the boundary. This is because water needs to overcome all barriers on its way out.
-
Once a cell is visited, we know its final water level: Since we always process cells in order of increasing height from the boundaries, when we reach a cell, we've already found the optimal (minimum height) path to the boundary. Any other path would have a higher maximum height.
The algorithm essentially simulates water "flooding" inward from all boundaries simultaneously, with the water level at each point determined by the minimum height barrier needed to reach that point from any boundary. The difference between this water level and the actual ground height gives us the trapped water volume at each cell.
Solution Approach
The implementation uses a priority queue (min-heap) combined with BFS traversal to simulate water flooding from the boundaries inward:
1. Initialize the boundary:
- Create a visited matrix
vis
to track processed cells - Use a min-heap
pq
to store cells as tuples(height, row, col)
- Add all boundary cells (first/last row and first/last column) to the heap and mark them as visited
- These boundary cells represent where water can escape, so no water can be trapped there
2. Process cells from lowest to highest:
while pq: h, i, j = heappop(pq) # Get the cell with minimum height
- Extract the cell with the minimum height from the heap
- The variable
h
represents the water level at this position (the minimum height needed to reach the boundary)
3. Check all 4 adjacent neighbors:
dirs = (-1, 0, 1, 0, -1) # Compact way to represent 4 directions for a, b in pairwise(dirs): # Generates pairs: (-1,0), (0,1), (1,0), (0,-1) x, y = i + a, j + b # Calculate neighbor coordinates
- Use the
pairwise
function to generate direction pairs for up, right, down, left - For each unvisited neighbor within bounds
4. Calculate trapped water:
ans += max(0, h - heightMap[x][y])
- The water level at the current position is
h
(inherited from the path to boundary) - If
h > heightMap[x][y]
, water is trapped: volume =h - heightMap[x][y]
- If
h <= heightMap[x][y]
, no water is trapped (themax(0, ...)
handles this)
5. Update neighbor's water level:
heappush(pq, (max(h, heightMap[x][y]), x, y))
- The water level at the neighbor is the maximum of:
- Current water level
h
(water can't flow upward) - The neighbor's ground height
heightMap[x][y]
(water can't go below ground)
- Current water level
- Add this neighbor to the heap with its determined water level
6. Mark as visited:
vis[x][y] = True
- Ensure each cell is processed only once
- Since we process cells in order of increasing water level, the first time we reach a cell gives us the optimal (minimum) water level
The algorithm continues until all reachable cells are processed. The total trapped water volume is accumulated in ans
and returned as the final result.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach:
heightMap = [ [3, 3, 3], [3, 1, 3], [3, 3, 3] ]
Step 1: Initialize boundary cells
- Add all boundary cells to min-heap with their heights
- Mark them as visited (v = visited, . = unvisited)
Heap: [(3,0,0), (3,0,1), (3,0,2), (3,1,0), (3,1,2), (3,2,0), (3,2,1), (3,2,2)] Visited: [v, v, v] [v, ., v] [v, v, v]
Step 2: Process cells from heap (all boundary cells have height 3)
- Pop (3,0,0), check neighbors - only (1,1) is unvisited but it's not adjacent
- Pop (3,0,1), check neighbors:
- Neighbor (1,1) is unvisited
- Water level at current position = 3
- Ground height at (1,1) = 1
- Trapped water = 3 - 1 = 2 units
- Add (1,1) to heap with water level max(3, 1) = 3
- Mark (1,1) as visited
Total water trapped: 2 Heap: [(3,0,2), (3,1,0), (3,1,2), (3,2,0), (3,2,1), (3,2,2), (3,1,1)] Visited: [v, v, v] [v, v, v] [v, v, v]
Step 3: Continue processing remaining cells
- All remaining cells in heap are boundary cells or already visited
- No more unvisited neighbors to process
- Algorithm terminates
Final Result: 2 units of water trapped
The center cell (1,1) with height 1 is surrounded by walls of height 3. Water fills up to level 3 (the minimum height needed to escape to the boundary), trapping 3 - 1 = 2 units of water above the center cell.
This demonstrates how the algorithm:
- Starts from all boundaries simultaneously
- Processes cells in order of increasing height
- Determines water level based on the minimum barrier to reach the boundary
- Calculates trapped water as the difference between water level and ground height
Solution Implementation
1class Solution:
2 def trapRainWater(self, heightMap: List[List[int]]) -> int:
3 # Get dimensions of the height map
4 rows, cols = len(heightMap), len(heightMap[0])
5
6 # Track visited cells to avoid processing them multiple times
7 visited = [[False] * cols for _ in range(rows)]
8
9 # Min-heap to process cells by height (water flows from lowest boundary)
10 # Each element is (height, row, col)
11 min_heap = []
12
13 # Add all boundary cells to the heap as starting points
14 # Water cannot be trapped at boundaries - it flows out
15 for row in range(rows):
16 for col in range(cols):
17 if row == 0 or row == rows - 1 or col == 0 or col == cols - 1:
18 heappush(min_heap, (heightMap[row][col], row, col))
19 visited[row][col] = True
20
21 # Total water trapped
22 total_water = 0
23
24 # Direction vectors for exploring 4 adjacent cells (up, right, down, left)
25 directions = (-1, 0, 1, 0, -1)
26
27 # Process cells from lowest boundary height to highest
28 while min_heap:
29 # Get the cell with minimum height from boundary
30 current_height, current_row, current_col = heappop(min_heap)
31
32 # Check all 4 adjacent cells
33 for i in range(4):
34 next_row = current_row + directions[i]
35 next_col = current_col + directions[i + 1]
36
37 # Check if the adjacent cell is valid and unvisited
38 if (0 <= next_row < rows and
39 0 <= next_col < cols and
40 not visited[next_row][next_col]):
41
42 # Water trapped = difference between current boundary height and cell height
43 # If cell is higher than boundary, no water is trapped (max with 0)
44 total_water += max(0, current_height - heightMap[next_row][next_col])
45
46 # Mark as visited
47 visited[next_row][next_col] = True
48
49 # Add to heap with height = max(boundary height, cell height)
50 # This maintains the water level for inner cells
51 heappush(min_heap, (max(current_height, heightMap[next_row][next_col]),
52 next_row, next_col))
53
54 return total_water
55
1class Solution {
2 public int trapRainWater(int[][] heightMap) {
3 // Get dimensions of the height map
4 int rows = heightMap.length;
5 int cols = heightMap[0].length;
6
7 // Track visited cells to avoid processing them multiple times
8 boolean[][] visited = new boolean[rows][cols];
9
10 // Min heap to process cells by height (lowest first)
11 // Each element: [height, row, col]
12 PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> a[0] - b[0]);
13
14 // Add all boundary cells to the priority queue as starting points
15 // These cells cannot trap water as they are on the edges
16 for (int i = 0; i < rows; i++) {
17 for (int j = 0; j < cols; j++) {
18 if (i == 0 || i == rows - 1 || j == 0 || j == cols - 1) {
19 minHeap.offer(new int[] {heightMap[i][j], i, j});
20 visited[i][j] = true;
21 }
22 }
23 }
24
25 // Initialize result to store total trapped water
26 int totalWater = 0;
27
28 // Direction vectors for moving up, right, down, left
29 int[] directions = {-1, 0, 1, 0, -1};
30
31 // Process cells from lowest height to highest (water flows from high to low)
32 while (!minHeap.isEmpty()) {
33 int[] current = minHeap.poll();
34 int currentHeight = current[0];
35 int currentRow = current[1];
36 int currentCol = current[2];
37
38 // Check all 4 adjacent cells
39 for (int k = 0; k < 4; k++) {
40 int nextRow = currentRow + directions[k];
41 int nextCol = currentCol + directions[k + 1];
42
43 // Check if the adjacent cell is within bounds and unvisited
44 if (nextRow >= 0 && nextRow < rows &&
45 nextCol >= 0 && nextCol < cols &&
46 !visited[nextRow][nextCol]) {
47
48 // Calculate water trapped at this cell
49 // Water level is determined by the minimum boundary height encountered so far
50 totalWater += Math.max(0, currentHeight - heightMap[nextRow][nextCol]);
51
52 // Mark cell as visited
53 visited[nextRow][nextCol] = true;
54
55 // Add cell to queue with updated height (max of current water level and cell height)
56 // This ensures water level never decreases as we move inward
57 minHeap.offer(new int[] {
58 Math.max(currentHeight, heightMap[nextRow][nextCol]),
59 nextRow,
60 nextCol
61 });
62 }
63 }
64 }
65
66 return totalWater;
67 }
68}
69
1class Solution {
2public:
3 int trapRainWater(vector<vector<int>>& heightMap) {
4 // Using a min-heap to process cells by height (lowest first)
5 // tuple: (height, row, col)
6 using HeightPosition = tuple<int, int, int>;
7 priority_queue<HeightPosition, vector<HeightPosition>, greater<HeightPosition>> minHeap;
8
9 int rows = heightMap.size();
10 int cols = heightMap[0].size();
11
12 // Track visited cells to avoid revisiting
13 bool visited[rows][cols];
14 memset(visited, 0, sizeof(visited));
15
16 // Add all boundary cells to the heap as starting points
17 // Water can flow out from boundaries, so they determine water levels
18 for (int row = 0; row < rows; ++row) {
19 for (int col = 0; col < cols; ++col) {
20 if (row == 0 || row == rows - 1 || col == 0 || col == cols - 1) {
21 minHeap.emplace(heightMap[row][col], row, col);
22 visited[row][col] = true;
23 }
24 }
25 }
26
27 int totalWater = 0;
28
29 // Direction vectors for moving up, right, down, left
30 int directions[5] = {-1, 0, 1, 0, -1};
31
32 // Process cells from lowest to highest height
33 while (!minHeap.empty()) {
34 // Get the cell with minimum height from the boundary
35 auto [currentHeight, currentRow, currentCol] = minHeap.top();
36 minHeap.pop();
37
38 // Check all 4 adjacent cells
39 for (int dir = 0; dir < 4; ++dir) {
40 int nextRow = currentRow + directions[dir];
41 int nextCol = currentCol + directions[dir + 1];
42
43 // If the adjacent cell is valid and not visited
44 if (nextRow >= 0 && nextRow < rows &&
45 nextCol >= 0 && nextCol < cols &&
46 !visited[nextRow][nextCol]) {
47
48 // Water trapped = difference between water level and ground height
49 // Water level is determined by the minimum boundary height encountered
50 totalWater += max(0, currentHeight - heightMap[nextRow][nextCol]);
51
52 visited[nextRow][nextCol] = true;
53
54 // Add the cell to heap with updated water level
55 // The water level for this cell becomes the max of its height and current water level
56 minHeap.emplace(max(heightMap[nextRow][nextCol], currentHeight), nextRow, nextCol);
57 }
58 }
59 }
60
61 return totalWater;
62 }
63};
64
1function trapRainWater(heightMap: number[][]): number {
2 // Using a min-heap to process cells by height (lowest first)
3 // Array format: [height, row, col]
4 type HeightPosition = [number, number, number];
5 const minHeap: HeightPosition[] = [];
6
7 // Helper function to maintain min-heap property
8 const heapPush = (item: HeightPosition): void => {
9 minHeap.push(item);
10 let currentIndex = minHeap.length - 1;
11
12 // Bubble up to maintain min-heap property
13 while (currentIndex > 0) {
14 const parentIndex = Math.floor((currentIndex - 1) / 2);
15 if (minHeap[currentIndex][0] < minHeap[parentIndex][0]) {
16 [minHeap[currentIndex], minHeap[parentIndex]] = [minHeap[parentIndex], minHeap[currentIndex]];
17 currentIndex = parentIndex;
18 } else {
19 break;
20 }
21 }
22 };
23
24 // Helper function to extract minimum element from heap
25 const heapPop = (): HeightPosition => {
26 const minItem = minHeap[0];
27 minHeap[0] = minHeap[minHeap.length - 1];
28 minHeap.pop();
29
30 if (minHeap.length === 0) return minItem;
31
32 // Bubble down to maintain min-heap property
33 let currentIndex = 0;
34 while (true) {
35 let smallestIndex = currentIndex;
36 const leftChild = 2 * currentIndex + 1;
37 const rightChild = 2 * currentIndex + 2;
38
39 if (leftChild < minHeap.length && minHeap[leftChild][0] < minHeap[smallestIndex][0]) {
40 smallestIndex = leftChild;
41 }
42 if (rightChild < minHeap.length && minHeap[rightChild][0] < minHeap[smallestIndex][0]) {
43 smallestIndex = rightChild;
44 }
45
46 if (smallestIndex !== currentIndex) {
47 [minHeap[currentIndex], minHeap[smallestIndex]] = [minHeap[smallestIndex], minHeap[currentIndex]];
48 currentIndex = smallestIndex;
49 } else {
50 break;
51 }
52 }
53
54 return minItem;
55 };
56
57 const rows = heightMap.length;
58 const cols = heightMap[0].length;
59
60 // Track visited cells to avoid revisiting
61 const visited: boolean[][] = Array(rows).fill(null).map(() => Array(cols).fill(false));
62
63 // Add all boundary cells to the heap as starting points
64 // Water can flow out from boundaries, so they determine water levels
65 for (let row = 0; row < rows; row++) {
66 for (let col = 0; col < cols; col++) {
67 if (row === 0 || row === rows - 1 || col === 0 || col === cols - 1) {
68 heapPush([heightMap[row][col], row, col]);
69 visited[row][col] = true;
70 }
71 }
72 }
73
74 let totalWater = 0;
75
76 // Direction vectors for moving up, right, down, left
77 const directions = [-1, 0, 1, 0, -1];
78
79 // Process cells from lowest to highest height
80 while (minHeap.length > 0) {
81 // Get the cell with minimum height from the boundary
82 const [currentHeight, currentRow, currentCol] = heapPop();
83
84 // Check all 4 adjacent cells
85 for (let dir = 0; dir < 4; dir++) {
86 const nextRow = currentRow + directions[dir];
87 const nextCol = currentCol + directions[dir + 1];
88
89 // If the adjacent cell is valid and not visited
90 if (nextRow >= 0 && nextRow < rows &&
91 nextCol >= 0 && nextCol < cols &&
92 !visited[nextRow][nextCol]) {
93
94 // Water trapped = difference between water level and ground height
95 // Water level is determined by the minimum boundary height encountered
96 totalWater += Math.max(0, currentHeight - heightMap[nextRow][nextCol]);
97
98 visited[nextRow][nextCol] = true;
99
100 // Add the cell to heap with updated water level
101 // The water level for this cell becomes the max of its height and current water level
102 heapPush([Math.max(heightMap[nextRow][nextCol], currentHeight), nextRow, nextCol]);
103 }
104 }
105 }
106
107 return totalWater;
108}
109
Time and Space Complexity
Time Complexity: O(m * n * log(m * n))
The algorithm uses a min-heap (priority queue) to process cells from the boundary inward. Breaking down the operations:
- Initially, all boundary cells are added to the heap:
O((2m + 2n - 4))
insertions, each takingO(log k)
wherek
is the current heap size - In the worst case, every cell in the grid is visited exactly once and added to the heap:
O(m * n)
cells total - For each cell popped from the heap, we check up to 4 neighbors (constant time)
- Each heap operation (push/pop) takes
O(log(m * n))
in the worst case when the heap contains all boundary cells - Total operations:
m * n
pops and up tom * n
pushes, each costingO(log(m * n))
Therefore, the overall time complexity is O(m * n * log(m * n))
.
Space Complexity: O(m * n)
The space usage consists of:
vis
array:O(m * n)
to track visited cells- Priority queue
pq
: In the worst case, it can contain up toO(m + n)
elements at any time (typically boundary cells and their immediate neighbors), but theoretically could hold up toO(m * n)
elements if we consider pathological cases - Recursive call stack: Not applicable as this is an iterative solution
- Other variables:
O(1)
for constants and loop variables
The dominant factor is the visited array and potentially the heap, giving us O(m * n)
space complexity.
Common Pitfalls
1. Incorrect Water Level Propagation
Pitfall: A common mistake is using the cell's own height instead of maintaining the maximum water level when adding neighbors to the heap.
Incorrect approach:
# WRONG: This doesn't maintain the water barrier height heappush(min_heap, (heightMap[next_row][next_col], next_row, next_col))
Why it's wrong: When water flows inward from boundaries, it maintains a minimum height based on the barriers it had to cross. If we only use the cell's ground height, we lose track of the water level established by outer barriers.
Correct approach:
# RIGHT: Maintain the maximum of current water level and ground height
heappush(min_heap, (max(current_height, heightMap[next_row][next_col]), next_row, next_col))
2. Processing Cells Multiple Times
Pitfall: Forgetting to mark cells as visited or checking visited status incorrectly can lead to cells being processed multiple times, resulting in counting trapped water multiple times.
Incorrect approach:
# WRONG: Marking visited after popping from heap while min_heap: current_height, current_row, current_col = heappop(min_heap) visited[current_row][current_col] = True # Too late! # ... rest of the code
Why it's wrong: By the time we pop a cell from the heap, it might have been added multiple times from different neighbors, leading to duplicate processing.
Correct approach:
# RIGHT: Mark visited immediately when adding to heap
if not visited[next_row][next_col]:
visited[next_row][next_col] = True # Mark immediately
heappush(min_heap, (max(current_height, heightMap[next_row][next_col]), next_row, next_col))
3. Using Maximum Heap Instead of Minimum Heap
Pitfall: Using a max-heap or processing cells from highest to lowest height.
Why it's wrong: Water naturally flows from low to high barriers. By processing from the lowest boundary points first, we correctly simulate how water would fill up the terrain. Processing from highest points would give incorrect water levels.
Visual Example: Consider a simple 3x3 grid:
5 5 5 5 1 5 5 5 5
- With min-heap (correct): Process boundaries first (all 5s), then inner cell. Water level = 5, trapped = 5-1 = 4
- With max-heap (wrong): Would incorrectly calculate water levels
4. Edge Case: Small Grids
Pitfall: Not handling grids smaller than 3x3 properly.
Why it matters: Grids with dimensions less than 3x3 cannot trap any water since all cells are on the boundary.
Solution:
def trapRainWater(self, heightMap: List[List[int]]) -> int:
rows, cols = len(heightMap), len(heightMap[0])
# Early return for grids too small to trap water
if rows < 3 or cols < 3:
return 0
# ... rest of the implementation
Which of the following shows the order of node visit in a Breadth-first Search?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!