Trapping Rain Water II solution and explanation
9 more videos on this problem
Trapping Rain Water II - Leetcode 407 - PythonNeetCodeIO · 15:19 · 30K views
Trapping Rain Water II | Deep Dive Explanation | What | Why | How | Leetcode 407 | codestorywithMIKcodestorywithMIK · 59:17 · 26K views
Trapping Rain Water II | Leetcode 407Techdose · 31:10 · 13K views
LeetCode 407. Trapping Rain Water II | Hard | Algorithm Explained | C++Cherry Coding [IIT-G] · 15:54 · 12K views
LeetCode 407. Trapping Rain Water II Explanation and Solutionhappygirlzt · 14:33 · 9K views
407. Trapping Rain Water II | BFS | Priority QueueAryan Mittal · 21:36 · 7K views
407. Trapping Rain Water II | leetcode daily challenge | shashcode | java | dsaShashCode · 22:30 · 3K views
Trapping Rain Water II | LeetCode 407 | Complete Intuition ExplainedSanyam IIT Guwahati · 16:30 · 2K views
407. Trapping Rain Water II (Leetcode Hard)Programming Live with Larry · 1:13:18 · 1K views
LeetCode 407. Trapping Rain Water II宰相小甘罗 · 5:28 · 298 views
The problem
Given an m x n integer matrix heightMap representing the height of each unit cell in a 2D elevation map, return the volume of water it can trap after raining.
Example 1:
Input: heightMap = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]] Output: 4 Explanation: After the rain, water is trapped between the blocks. We have two small ponds 1 and 3 units trapped. The total volume of water trapped is 4.
Example 2:
Input: heightMap = [[3,3,3,3,3],[3,2,2,2,3],[3,2,1,2,3],[3,2,2,2,3],[3,3,3,3,3]] Output: 10
Constraints:
m == heightMap.lengthn == heightMap[i].length1 <= m, n <= 2000 <= heightMap[i][j] <= 2 * 104
Approach overview
Problem Overview: You are given a 2D grid where each cell represents elevation. After rainfall, water can be trapped between taller boundary cells. The task is to compute how many total units of water remain trapped after the system stabilizes.
Approach 1: Min-Heap Priority Queue + BFS (O(mn log(mn)) time, O(mn) space)
This problem behaves like filling water from the outside boundary inward. Start by pushing all boundary cells into a min-heap. The heap always expands from the lowest boundary height first. Pop the lowest cell, explore its four neighbors using a BFS-style traversal, and determine whether water can be trapped there. If a neighbor is lower than the current boundary height, the difference is trapped water. Push the neighbor back into the heap with an effective height of max(currentHeight, neighborHeight). This simulates raising the water level as the basin fills.
The key insight: the lowest boundary always determines the maximum water level of the region it expands into. Using a heap ensures cells are processed in increasing height order. Each cell is visited once and inserted into the heap at most once, giving O(mn log(mn)) time due to heap operations and O(mn) space for the visited matrix and heap. This approach combines ideas from Heap (Priority Queue), Breadth-First Search, and grid traversal.
Approach 2: Flood Fill Simulation (O(mn log(mn)) time, O(mn) space)
Another perspective treats the problem as a flood-fill from the outer boundary. Initialize the boundary as the starting frontier and gradually propagate inward. Instead of thinking about trapping water locally, maintain the current water level defined by the smallest boundary cell encountered. Each step expands to neighboring cells and updates the water level accordingly. If a cell is lower than the active water level, the difference contributes to trapped volume.
The flood fill behaves similarly to a Dijkstra-style expansion across the grid. The frontier must still be processed in increasing elevation order, so a priority queue is typically used internally. The algorithm marks cells as visited to prevent revisiting and keeps updating the effective boundary height as it expands inward. Complexity remains O(mn log(mn)) time with O(mn) auxiliary space.
This formulation helps when reasoning about water propagation in a matrix environment. It also clarifies why the outer boundary must be processed first: water can always escape through the lowest surrounding wall.
Recommended for interviews: The min-heap boundary expansion approach is what interviewers expect. It demonstrates understanding of priority queues, BFS-style grid traversal, and the key insight that the minimum boundary controls the water level. Explaining the flood-fill intuition helps justify the algorithm, but implementing the heap-based BFS shows stronger problem-solving skill.
Approach 1: Using a Min-Heap Priority Queue
This approach utilizes a min-heap (priority queue) to efficiently determine the minimum boundary height around a smaller section of the map. Initially, all the border points are pushed into the heap. We then process each cell in the heap by examining its neighbors. If the height of a neighboring cell is lower, it indicates that water can be trapped above it. The heap is utilized to ensure that the next cell to process is the one which determines the lowest possible trapped water boundary for its neighbors.
import heapq
def trapRainWater(heightMap):
if not heightMap or not heightMap[0]:
return 0
m, n = len(heightMap), len(heightMap[0])
visited = [[False] * n for _ in range(m)]
heap = []
# Push all the boundary cells into the heap
for i in range(m):
for j in range(n):
if i == 0 or j == 0 or i == m - 1 or j == n - 1:
heapq.heappush(heap, (heightMap[i][j], i, j))
visited[i][j] = True
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
trappedWater = 0
while heap:
height, x, y = heapq.heappop(heap)
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < m and 0 <= ny < n and not visited[nx][ny]:
trappedWater += max(0, height - heightMap[nx][ny])
heapq.heappush(heap, (max(height, heightMap[nx][ny]), nx, ny))
visited[nx][ny] = True
return trappedWaterimport java.util.PriorityQueue;
class Solution {
private class Cell {
int height, x, y;
Cell(int h, int i, int j) { this.height = h; this.x = i; this.y = j; }
}
public int trapRainWater(int[][] heightMap) {
if (heightMap == null || heightMap.length == 0 || heightMap[0].length == 0) return 0;
int m = heightMap.length, n = heightMap[0].length;
PriorityQueue<Cell> pq = new PriorityQueue<>((a, b) -> a.height - b.height);
boolean[][] visited = new boolean[m][n];
// Push all the boundary cells into the heap
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0 || i == m - 1 || j == n - 1) {
pq.add(new Cell(heightMap[i][j], i, j));
visited[i][j] = true;
}
}
}
int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int trappedWater = 0;
while (!pq.isEmpty()) {
Cell cell = pq.poll();
for (int[] d : directions) {
int nx = cell.x + d[0], ny = cell.y + d[1];
if (nx < 0 || nx >= m || ny < 0 || ny >= n || visited[nx][ny]) continue;
trappedWater += Math.max(0, cell.height - heightMap[nx][ny]);
pq.add(new Cell(Math.max(cell.height, heightMap[nx][ny]), nx, ny));
visited[nx][ny] = true;
}
}
return trappedWater;
}
}#include <vector>
#include <queue>
#include <functional>
using namespace std;
class Solution {
public:
struct Cell {
int x, y, height;
Cell(int height, int x, int y) : height(height), x(x), y(y) {}
bool operator>(const Cell& other) const { return height > other.height; }
};
int trapRainWater(vector<vector<int>>& heightMap) {
if (heightMap.empty()) return 0;
int m = heightMap.size(), n = heightMap[0].size();
priority_queue<Cell, vector<Cell>, greater<Cell>> pq;
vector<vector<bool>> visited(m, vector<bool>(n, false));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (i == 0 || j == 0 || i == m - 1 || j == n - 1) {
pq.push(Cell(heightMap[i][j], i, j));
visited[i][j] = true;
}
}
}
int directions[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int trappedWater = 0;
while (!pq.empty()) {
Cell cell = pq.top(); pq.pop();
for (auto& dir : directions) {
int nx = cell.x + dir[0], ny = cell.y + dir[1];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && !visited[nx][ny]) {
trappedWater += max(0, cell.height - heightMap[nx][ny]);
pq.push(Cell(max(cell.height, heightMap[nx][ny]), nx, ny));
visited[nx][ny] = true;
}
}
}
return trappedWater;
}
};var trapRainWater = function(heightMap) {
if (!heightMap || !heightMap.length || !heightMap[0].length) return 0;
const m = heightMap.length, n = heightMap[0].length;
const visited = Array.from({ length: m }, () => Array(n).fill(false));
const heap = new MinPriorityQueue({ priority: x => x.height });
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (i === 0 || j === 0 || i === m - 1 || j === n - 1) {
heap.enqueue({ height: heightMap[i][j], x: i, y: j });
visited[i][j] = true;
}
}
}
const directions = [[1, 0], [-1, 0], [0, 1], [0, -1]];
let trappedWater = 0;
while (!heap.isEmpty()) {
const { element: { height, x, y } } = heap.dequeue();
for (const [dx, dy] of directions) {
const nx = x + dx, ny = y + dy;
if (nx >= 0 && nx < m && ny >= 0 && ny < n && !visited[nx][ny]) {
trappedWater += Math.max(0, height - heightMap[nx][ny]);
heap.enqueue({ height: Math.max(height, heightMap[nx][ny]), x: nx, y: ny });
visited[nx][ny] = true;
}
}
}
return trappedWater;
};using System;
using System.Collections.Generic;
public class Solution {
public class Cell : IComparable<Cell> {
public int height, row, col;
public Cell(int h, int r, int c) { height = h; row = r; col = c; }
public int CompareTo(Cell other) => height.CompareTo(other.height);
}
public int TrapRainWater(int[][] heightMap) {
if (heightMap == null || heightMap.Length == 0 || heightMap[0].Length == 0) return 0;
int m = heightMap.Length, n = heightMap[0].Length;
var pq = new SortedSet<Cell>();
bool[,] visited = new bool[m, n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0 || i == m - 1 || j == n - 1) {
pq.Add(new Cell(heightMap[i][j], i, j));
visited[i, j] = true;
}
}
}
int[][] directions = new int[][] {
new int[] {1, 0}, new int[] {-1, 0},
new int[] {0, 1}, new int[] {0, -1}
};
int trappedWater = 0;
while (pq.Count > 0) {
var cell = pq.Min;
pq.Remove(cell);
foreach (var dir in directions) {
int nx = cell.row + dir[0], ny = cell.col + dir[1];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && !visited[nx, ny]) {
trappedWater += Math.Max(0, cell.height - heightMap[nx][ny]);
pq.Add(new Cell(Math.Max(cell.height, heightMap[nx][ny]), nx, ny));
visited[nx, ny] = true;
}
}
}
return trappedWater;
}
}The function first initializes a priority queue with all boundary cells, marking them as visited. Cells are processed from the queue in order of increasing height. For each cell, its neighbors are checked to determine if water can be trapped. If so, water is added, and the neighbor cell is pushed into the queue as potentially trapping more water.
Time Complexity: O(m * n * log(m * n)), where m is the number of rows and n is the number of columns, due to priority queue operations.
Space Complexity: O(m * n) because of the auxiliary structures used for visited tracking and min-heap storage.
Approach 2: Flood Fill Technique
This approach employs a flood-fill technique starting from the boundary cells and leverages depth-first or breadth-first search to explore the inward cells. The idea is similar to how flood-fill works in graphics, where all connected cells are considered to be part of the same region and receive the same 'flood' level. This approach ensures that all accessible cells connected through any boundary are given a chance to receive water, effectively checking for possible traps.
from collections import deque
def trapRainWater(heightMap):
if not heightMap or not heightMap[0]:
return 0
m, n = len(heightMap), len(heightMap[0])
visited = [[False] * n for _ in range(m)]
bfs_queue = deque()
# Initialize BFS queue with border cells
for i in range(m):
for j in range(n):
if i == 0 or j == 0 or i == m - 1 or j == n - 1:
bfs_queue.append((i, j))
visited[i][j] = True
trappedWater = 0
while bfs_queue:
x, y = bfs_queue.popleft()
for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
nx, ny = x + dx, y + dy
if 0 <= nx < m and 0 <= ny < n and not visited[nx][ny]:
if heightMap[nx][ny] < heightMap[x][y]:
trappedWater += heightMap[x][y] - heightMap[nx][ny]
heightMap[nx][ny] = heightMap[x][y] # Update the height to mark flooded level
bfs_queue.append((nx, ny))
visited[nx][ny] = True
return trappedWaterThis solution uses a BFS queue to perform the flood-fill operation on the height map. Each cell on the border is considered starting point, and its height is used to fill inward cells until no more water can be trapped. The design tracks visited cells and updates levels using the fill technique to maintain overflow boundaries.
Time Complexity: O(m * n), where m is the number of rows and n is the number of columns, given the potential need to inspect each cell.
Space Complexity: O(m * n) to handle the visited array and BFS queue.
Approach 3: Priority Queue (Min Heap)
This is a variant of the trapping rain water problem. Since the heights on the matrix boundaries are fixed, we can add these boundary heights to a priority queue. Then, we repeatedly take out the minimum height from the priority queue and compare it with the heights of its four adjacent cells. If an adjacent cell's height is less than the current height, we can trap water there. The volume of trapped water is the difference between the current height and the adjacent height. We then add the larger height back to the priority queue and repeat this process until the priority queue is empty.
The time complexity is O(m times n times log (m times n)), and the space complexity is O(m times n), where m and n are the number of rows and columns in the matrix, respectively.
function trapRainWater(heightMap: number[][]): number {
const m = heightMap.length;
const n = heightMap[0].length;
const vis: boolean[][] = Array.from({ length: m }, () => new Array(n).fill(false));
const pq = new PriorityQueue<number[]>((a, b) => a[0] - b[0]);
for (let i = 0; i < m; ++i) {
for (let j = 0; j < n; ++j) {
if (i === 0 || i === m - 1 || j === 0 || j === n - 1) {
pq.enqueue([heightMap[i][j], i, j]);
vis[i][j] = true;
}
}
}
let ans = 0;
const dirs = [-1, 0, 1, 0, -1];
while (!pq.isEmpty()) {
const [h, i, j] = pq.dequeue();
for (let k = 0; k < 4; ++k) {
const x = i + dirs[k];
const y = j + dirs[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n && !vis[x][y]) {
ans += Math.max(0, h - heightMap[x][y]);
vis[x][y] = true;
pq.enqueue([Math.max(h, heightMap[x][y]), x, y]);
}
}
}
return ans;
}func trapRainWater(heightMap [][]int) (ans int) {
m, n := len(heightMap), len(heightMap[0])
pq := hp{}
vis := make([][]bool, m)
for i, row := range heightMap {
vis[i] = make([]bool, n)
for j, v := range row {
if i == 0 || i == m-1 || j == 0 || j == n-1 {
heap.Push(&pq, tuple{v, i, j})
vis[i][j] = true
}
}
}
dirs := []int{-1, 0, 1, 0, -1}
for len(pq) > 0 {
p := heap.Pop(&pq).(tuple)
for k := 0; k < 4; k++ {
x, y := p.i+dirs[k], p.j+dirs[k+1]
if x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] {
ans += max(0, p.v-heightMap[x][y])
vis[x][y] = true
heap.Push(&pq, tuple{max(p.v, heightMap[x][y]), x, y})
}
}
}
return
}
type tuple struct{ v, i, j int }
type hp []tuple
func (h hp) Len() int { return len(h) }
func (h hp) Less(i, j int) bool { return h[i].v < h[j].v }
func (h hp) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *hp) Push(v any) { *h = append(*h, v.(tuple)) }
func (h *hp) Pop() any { a := *h; v := a[len(a)-1]; *h = a[:len(a)-1]; return v }Complexity comparison
| Approach | Complexity |
|---|---|
| Using a Min-Heap Priority Queue | Time Complexity: O(m * n * log(m * n)), where m is the number of rows and n is the number of columns, due to priority queue operations. |
| Flood Fill Technique | Time Complexity: O(m * n), where m is the number of rows and n is the number of columns, given the potential need to inspect each cell. |
| Priority Queue (Min Heap) | – |
Complexity analysis
| Approach | Time | Space | When to Use |
|---|---|---|---|
| Min-Heap Priority Queue + BFS | O(mn log(mn)) | O(mn) | Standard optimal solution for 2D trapping rainwater problems. Expected in interviews. |
| Flood Fill Boundary Expansion | O(mn log(mn)) | O(mn) | Useful for reasoning about water propagation across a grid or explaining the heap solution. |
Frequently asked questions
Is Trapping Rain Water II easy or hard?
Trapping Rain Water II is classified as Hard on LeetCode. The difficulty comes from extending the 1D trapping rainwater concept into a 2D grid and recognizing that the solution requires a priority queue with BFS-style expansion.
How to solve Trapping Rain Water II in O(n)?
A true O(n) solution is not known for the 2D version of the problem. Unlike the 1D trapping rain water problem, the grid requires maintaining a global boundary order. The most efficient practical algorithm uses a min-heap and runs in O(mn log(mn)).
What is the best approach for Trapping Rain Water II?
The best approach uses a min-heap priority queue combined with BFS. All boundary cells are pushed into a heap, and the algorithm expands inward from the lowest boundary first. This ensures the current minimum wall height always determines how much water can be trapped. The time complexity is O(mn log(mn)) with O(mn) space.
What data structure is used in Trapping Rain Water II?
The core data structure is a min-heap (priority queue). It always processes the lowest boundary cell first, which determines the current water level. A visited matrix and BFS-style neighbor traversal are also used to explore the grid safely.
What is the time complexity of Trapping Rain Water II?
The optimal solution runs in O(mn log(mn)) time where m and n are grid dimensions. Each cell is processed once, but heap insertions and removals cost O(log(mn)). Space complexity is O(mn) for the visited matrix and priority queue.
Trapping Rain Water II Python or Java solution approach?
Python solutions typically use heapq with a visited matrix and directional traversal. Java implementations use PriorityQueue with a custom comparator for cell heights. Both follow the same boundary-expansion algorithm with O(mn log(mn)) complexity.
Is Trapping Rain Water II asked at Google or Amazon interviews?
Trapping Rain Water II appears in advanced interview rounds at companies like Google, Amazon, and Meta because it tests priority queues, BFS traversal, and spatial reasoning on grids. Candidates must combine multiple concepts rather than applying a single pattern.
