Trapping Rain Water II

Updated on 30 June, 2025
Trapping Rain Water II header image

Problem Statement

The task revolves around calculating the volume of water trapped on a 2D topographic map after rainfall. Specifically, we are given a matrix named heightMap where each element represents the elevation of a square unit cell of the terrain. The goal is to calculate the total volume of water that remains trapped between the elevated regions of the map post-rain. The matrix dimensions, m x n, depict the size of the elevation map. This computational challenge involves dealing with spatial structures on a grid and determining the interaction between them when subjected to water accumulation scenarios.

Examples

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.length
  • n == heightMap[i].length
  • 1 <= m, n <= 200
  • 0 <= heightMap[i][j] <= 2 * 104

Approach and Intuition

Understanding the water trapping on a 2D map can be visualized similar to a 3D landscape where rain falls uniformly over the area. Post-rain, some areas of the map, depending on their surrounding elevation, will collect and hold water. Here's how we can intuit the process:

  1. Identify low points in the map where water is likely to collect. These points will typically be surrounded by higher points on all sides.
  2. Expand outward from these initial low points, checking neighbors to determine if they can also hold water without spilling over to a lower height.
  3. As we inspect each cell, calculate the water volume it can hold by comparing its height with its immediate surroundings. The difference in height, if surrounded by higher cells, determines the depth of the trapped water in that cell.
  4. Accumulate the volume for all cells that trap water.
  • Example Interpretations based on the examples provided:

    • For example 1 from the input [[1,4,3,1,3,2], [3,2,1,3,2,4], [2,3,3,2,3,1]], visualize a small basin where there are pockets (depicted by lower numbers surrounded by higher numbers). After simulating rain, these pockets trap water. The volume, in this case, was calculated as 4 units.
    • In a more substantial case like example 2 with a deeper center at level 1 and surrounded by walls of 3s, the water trapped is greater. The calculation provided the trapped volume as 10 units, showcasing a more extensive central pool.
  • These examples adhere strictly to the constraints that the matrix is at least 1x1 and at most 200x200 with the heights of each cell ranging from 0 to 20000, ensuring that computations remain manageable yet potentially detailed and complex depending on the landscape given.

Solutions

  • C++
  • Java
  • Python
cpp
class Solution {
public:
    int calculateWaterTrapping(vector<vector<int>>& elevationMap) {
        // Direction vectors
        int deltaRow[4] = {0, 0, -1, 1};
        int deltaCol[4] = {-1, 1, 0, 0};
    
        int rows = elevationMap.size();
        int cols = elevationMap[0].size();
    
        vector<vector<bool>> seen(rows, vector<bool>(cols, false));
    
        // Min-heap to store the boundary cells
        priority_queue<WaterCell> edges;
    
        // Process the boundary by adding the first and last column of each row
        for (int r = 0; r < rows; r++) {
            edges.push(WaterCell(elevationMap[r][0], r, 0));
            edges.push(WaterCell(elevationMap[r][cols - 1], r, cols - 1));
            seen[r][0] = seen[r][cols - 1] = true;
        }
    
        // Process the boundary by adding the first and last row
        for (int c = 0; c < cols; c++) {
            edges.push(WaterCell(elevationMap[0][c], 0, c));
            edges.push(WaterCell(elevationMap[rows - 1][c], rows - 1, c));
            seen[0][c] = seen[rows - 1][c] = true;
        }
    
        int trappedWater = 0;
    
        while (!edges.empty()) {
            WaterCell cell = edges.top();
            edges.pop();
    
            int currentR = cell.row;
            int currentC = cell.col;
            int boundaryHeight = cell.height;
    
            for (int d = 0; d < 4; d++) {
                int nr = currentR + deltaRow[d];
                int nc = currentC + deltaCol[d];
    
                if (isWithinBounds(nr, nc, rows, cols) && !seen[nr][nc]) {
                    int neighborHeight = elevationMap[nr][nc];
    
                    if (neighborHeight < boundaryHeight) {
                        trappedWater += boundaryHeight - neighborHeight;
                    }
    
                    edges.push(WaterCell(max(neighborHeight, boundaryHeight), nr, nc));
                    seen[nr][nc] = true;
                }
            }
        }
    
        return trappedWater;
    }
    
private:
    class WaterCell {
    public:
        int height;
        int row;
        int col;
    
        WaterCell(int h, int r, int c) : height(h), row(r), col(c) {}
    
        bool operator<(const WaterCell& other) const {
            return height > other.height; // Min-heap by reversing comparison
        }
    };
    
    bool isWithinBounds(int row, int col, int maxRows, int maxCols) {
        return row >= 0 && col >= 0 && row < maxRows && col < maxCols;
    }
};

This solution addresses the problem of computing the volume of water that can be trapped in a 3D landscape represented by a 2D elevation map. The algorithm employs a priority queue (min-heap) to dynamically select the lowest boundary on the surface, and a breadth-first search strategy to visit and calculate trapped water for each cell. The solution is implemented in C++ and outlined below:

  • The problem is initiated by creating a priority queue to keep track of all cells on the perimeter of the elevation map, which are considered as initial boundaries. Cells are represented by an inner class WaterCell, which stores a cell's height along with its row and column index.
  • Every boundary cell is marked as seen or visited to prevent re-examination, using a 2D vector of boolean values.
  • From each current boundary cell, adjacent cells are examined in four cardinal directions (North, South, East, West). This process is facilitated by two arrays denoting row and column shifts.
  • For each adjacent cell, the algorithm checks if it’s within bounds and not yet visited. If the neighbor cell’s elevation is lower than the current boundary cell's elevation, the difference in elevation is treated as the volume of trapped water. The neighbor then becomes part of the boundary, and its height is adjusted to the greater value between its own height and the current boundary height; this ensures that future calculations respect the water surface created by earlier trappings.
  • The process repeats until all cells in the elevation map are either visited or considered as water trapping boundaries.
  • The accumulated trapped water is returned as the result.

This approach not only systematically expands the boundary inward but also ensures that the lowest edges are processed first, thus maximizing the trapped water calculation by making optimal local decisions that lead to the global optimum.

java
class WaterTrapSolver {
    
    public int calculateWater(int[][] elevationMap) {
        int[] rowDir = { -1, 1, 0, 0 };
        int[] colDir = { 0, 0, -1, 1 };
    
        int numRows = elevationMap.length;
        int numCols = elevationMap[0].length;
    
        boolean[][] visitedAreas = new boolean[numRows][numCols];
    
        PriorityQueue<GridCell> boundaryQueue = new PriorityQueue<>();
    
        for (int row = 0; row < numRows; row++) {
            boundaryQueue.offer(new GridCell(elevationMap[row][0], row, 0));
            boundaryQueue.offer(new GridCell(elevationMap[row][numCols - 1], row, numCols - 1));
            visitedAreas[row][0] = true;
            visitedAreas[row][numCols - 1] = true;
        }
    
        for (int col = 0; col < numCols; col++) {
            boundaryQueue.offer(new GridCell(elevationMap[0][col], 0, col));
            boundaryQueue.offer(new GridCell(elevationMap[numRows - 1][col], numRows - 1, col));
            visitedAreas[0][col] = true;
            visitedAreas[numRows - 1][col] = true;
        }
    
        int trappedWater = 0;
    
        while (!boundaryQueue.isEmpty()) {
            GridCell cell = boundaryQueue.poll();
    
            int row = cell.row;
            int col = cell.col;
            int boundaryHeight = cell.height;
    
            for (int d = 0; d < 4; d++) {
                int newRow = row + rowDir[d];
                int newCol = col + colDir[d];
    
                if (isInBounds(newRow, newCol, numRows, numCols) && !visitedAreas[newRow][newCol]) {
                    int newHeight = elevationMap[newRow][newCol];
    
                    if (newHeight < boundaryHeight) {
                        trappedWater += boundaryHeight - newHeight;
                    }
    
                    boundaryQueue.offer(new GridCell(Math.max(newHeight, boundaryHeight), newRow, newCol));
                    visitedAreas[newRow][newCol] = true;
                }
            }
        }
    
        return trappedWater;
    }
    
    private static class GridCell implements Comparable<GridCell> {
        int height;
        int row;
        int col;
    
        public GridCell(int height, int row, int col) {
            this.height = height;
            this.row = row;
            this.col = col;
        }
    
        @Override
        public int compareTo(GridCell other) {
            return Integer.compare(this.height, other.height);
        }
    }
    
    private boolean isInBounds(int row, int col, int numRows, int numCols) {
        return row >= 0 && col >= 0 && row < numRows && col < numCols;
    }
}

The provided Java solution for the "Trapping Rain Water II" problem calculates the total amount of water trapped in a 2D elevation map. The method employs a breadth-first search-like algorithm utilizing a priority queue to efficiently process grid cells based on their elevation from lowest to highest. This ensures that one always processes the lowest boundary to find potential trapped water.

  • The calculateWater method initializes direction arrays to facilitate movement in four directions on the grid (up, down, left, right).
  • A PriorityQueue (boundaryQueue) is used to store and sort the boundary cells based on their heights.
  • The boundaries of the elevation map grid are loaded into the queue first, ensuring that the edges are processed before inner cells.
  • The method iteratively processes each cell in the queue, examining its four neighboring cells. If the neighboring cell's elevation is lower than the processed cell's boundary height, the difference in height represents trapped water.
  • To simulate water overflowing and filling, the neighbor is then added to the queue with an updated maximum height of its original height or the boundary height, ensuring correct subsequent water trapping calculation.
  • A boolean matrix (visitedAreas) keeps track of cells already processed to avoid redundant calculations.

As each boundary cell is processed, marking it as visited and evaluating its neighbors for potential trapped water volumes efficiently simulate water filling the low-lying areas surrounded by higher elevations, leading to the total volume of trapped water being returned at the end. This method ensures that each cell is processed once, optimizing the solution's efficiency.

python
class WaterTrap:
    class GridCell:
        def __init__(self, h, x, y):
            self.h = h
            self.x = x
            self.y = y
    
        def __lt__(self, other):
            return self.h < other.h
    
    def _valid(self, x, y, xmax, ymax):
        return 0 <= x < xmax and 0 <= y < ymax
    
    def calculateWater(self, heights):
        d_x = [0, 0, -1, 1]
        d_y = [-1, 1, 0, 0]
    
        max_x = len(heights)
        max_y = len(heights[0])
    
        seen = [[False] * max_y for _ in range(max_x)]
    
        edges = []
    
        for i in range(max_x):
            heapq.heappush(edges, self.GridCell(heights[i][0], i, 0))
            heapq.heappush(edges, self.GridCell(heights[i][max_y - 1], i, max_y - 1))
            seen[i][0] = seen[i][max_y - 1] = True
    
        for j in range(max_y):
            heapq.heappush(edges, self.GridCell(heights[0][j], 0, j))
            heapq.heappush(edges, self.GridCell(heights[max_x - 1][j], max_x - 1, j))
            seen[0][j] = seen[max_x - 1][j] = True
    
        water_trapped = 0
    
        while edges:
            cell = heapq.heappop(edges)
    
            for d in range(4):
                new_x = cell.x + d_x[d]
                new_y = cell.y + d_y[d]
    
                if self._valid(new_x, new_y, max_x, max_y) and not seen[new_x][new_y]:
                    next_h = heights[new_x][new_y]
    
                    if next_h < cell.h:
                        water_trapped += cell.h - next_h
    
                    heapq.heappush(edges, self.GridCell(max(next_h, cell.h), new_x, new_y))
                    seen[new_x][new_y] = True
    
        return water_trapped

Trapping Rain Water II is a problem that calculates the volume of water trapped in a 3D surface represented as a 2D grid using the Python programming language. The solution involves constructing a min-heap to efficiently retrieve the lowest perimeter cell of the grid and using it as a starting point for water trapping calculations.

Here's how the implementation breaks down:

  • Define the GridCell class to represent a cell with a height and coordinates. Override the less than operator to compare cells based on height which helps in ordering the cells in a priority queue.
  • Create a helper function _valid to check whether given coordinates are within the grid boundary.
  • Initialize a calculateWater function to perform the main computation:
    1. Initialize variables and data structures, including the 2D list seen to keep track of processed cells and the priority queue edges for processing cells in order of their heights.
    2. Push all perimeter cells into the heap (edges) and mark them as seen.
    3. Process cells from the heap. For each cell processed, check its four neighboring cells:
      • Use directional arrays d_x and d_y to find neighboring cells.
      • If the neighboring cell is valid and unvisited:
        • Calculate trapped water if the neighboring cell's height is less than the current cell's height.
        • Push the neighboring cell (or an elevated version of it if the neighboring height is lower) into the heap for future processing.
        • Mark the neighboring cell as seen.

This method spreads from the perimeter inwards, ensuring that the shortest bars (grid cells) are processed first, allowing water trapping calculations between taller bars further inside the perimeter. The result is the sum of trapped water in all eligible cell gaps. This greedy algorithm solution is efficient due to the use of the priority queue and careful management of state with the seen list.

Comments

No comments yet.