Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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.

  2. 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.

  3. 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 (the max(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)
  • 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 Evaluator

Example 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:

  1. Starts from all boundaries simultaneously
  2. Processes cells in order of increasing height
  3. Determines water level based on the minimum barrier to reach the boundary
  4. 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 taking O(log k) where k 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 to m * n pushes, each costing O(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 to O(m + n) elements at any time (typically boundary cells and their immediate neighbors), but theoretically could hold up to O(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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More