问题描述
接雨水是经典的数组处理问题。给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子下雨之后能接多少雨水。
示例:
输入: height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
算法思路分析
方法一:暴力解法
对于每个位置,找到其左边最高和右边最高的柱子,该位置能接的雨水量为 min(maxLeft, maxRight) - height[i]。时间复杂度 O(n²)。
方法二:动态规划(预处理最值)
预先计算每个位置的左边最大值和右边最大值数组,将查询优化到 O(1),总时间复杂度 O(n),空间复杂度 O(n)。
方法三:双指针(最优空间)
维护左右两个指针和左右两边的最大高度。哪边的最大高度较小,就先处理哪边。时间复杂度 O(n),空间复杂度 O(1)。
方法四:单调栈
利用单调递减栈,当遇到更高的柱子时,弹出栈顶计算凹槽中的雨水量。可同时计算多个凹槽。
Java 完整实现
import java.util.ArrayDeque;
import java.util.Deque;
public class TrappingRainWater {
/**
* 方法一:动态规划(预处理左右最大值)
* 时间复杂度:O(n),空间复杂度:O(n)
*/
public int trapDP(int[] height) {
if (height == null || height.length < 3) return 0;
int n = height.length;
// leftMax[i] 表示位置 i 左侧(含 i)的最高柱子高度
int[] leftMax = new int[n];
leftMax[0] = height[0];
for (int i = 1; i < n; i++) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
// rightMax[i] 表示位置 i 右侧(含 i)的最高柱子高度
int[] rightMax = new int[n];
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
// 每个位置接水量 = min(左最高, 右最高) - 当前高度
int water = 0;
for (int i = 0; i < n; i++) {
water += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return water;
}
/**
* 方法二:双指针(空间优化版)
* 时间复杂度:O(n),空间复杂度:O(1)
*/
public int trapTwoPointers(int[] height) {
if (height == null || height.length < 3) return 0;
int n = height.length;
int left = 0, right = n - 1;
int leftMax = 0, rightMax = 0;
int water = 0;
while (left < right) {
if (height[left] < height[right]) {
// 左边较小,以 leftMax 为瓶颈计算
if (height[left] >= leftMax) {
leftMax = height[left]; // 更新左边最大
} else {
water += leftMax - height[left]; // 可以接水
}
left++;
} else {
// 右边较小(或相等),以 rightMax 为瓶颈计算
if (height[right] >= rightMax) {
rightMax = height[right]; // 更新右边最大
} else {
water += rightMax - height[right]; // 可以接水
}
right--;
}
}
return water;
}
/**
* 方法三:单调栈
* 时间复杂度:O(n),空间复杂度:O(n)
* 栈中保存的是柱子的下标,对应高度单调递减
*/
public int trapMonotonicStack(int[] height) {
if (height == null || height.length < 3) return 0;
int n = height.length;
Deque<Integer> stack = new ArrayDeque<>(); // 单调递减栈
int water = 0;
for (int i = 0; i < n; i++) {
// 当前柱子比栈顶高,可以形成凹槽接水
while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
int bottom = stack.pop(); // 凹槽底部
if (stack.isEmpty()) break; // 没有左边界
int left = stack.peek(); // 左边界下标
// 凹槽宽度 = 当前下标 - 左边界下标 - 1
int width = i - left - 1;
// 凹槽高度 = min(左边界高度, 右边界高度) - 底部高度
int h = Math.min(height[left], height[i]) - height[bottom];
water += width * h;
}
stack.push(i);
}
return water;
}
/**
* 扩展:接雨水 II(三维版本,接雨水II)
* 使用优先队列(最小堆)从边界向内部扩展
*/
public int trapRainWaterII(int[][] heightMap) {
if (heightMap == null || heightMap.length < 3 || heightMap[0].length < 3) {
return 0;
}
int rows = heightMap.length;
int cols = heightMap[0].length;
boolean[][] visited = new boolean[rows][cols];
// 最小堆:按高度排序,存储 (高度, 行, 列)
java.util.PriorityQueue<int[]> minHeap = new java.util.PriorityQueue<>(
(a, b) -> a[0] - b[0]
);
// 将边界加入堆
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (i == 0 || i == rows - 1 || j == 0 || j == cols - 1) {
minHeap.offer(new int[]{heightMap[i][j], i, j});
visited[i][j] = true;
}
}
}
int water = 0;
int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int maxBoundary = 0;
while (!minHeap.isEmpty()) {
int[] curr = minHeap.poll();
int h = curr[0], r = curr[1], c = curr[2];
maxBoundary = Math.max(maxBoundary, h);
for (int[] dir : dirs) {
int nr = r + dir[0], nc = c + dir[1];
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols || visited[nr][nc]) {
continue;
}
visited[nr][nc] = true;
// 如果内部格子低于当前边界,可以蓄水
if (heightMap[nr][nc] < maxBoundary) {
water += maxBoundary - heightMap[nr][nc];
}
minHeap.offer(new int[]{heightMap[nr][nc], nr, nc});
}
}
return water;
}
/**
* 可视化:打印每个位置的接水量
*/
public void visualize(int[] height) {
if (height == null || height.length == 0) return;
int n = height.length;
int[] leftMax = new int[n];
int[] rightMax = new int[n];
leftMax[0] = height[0];
for (int i = 1; i < n; i++) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
System.out.println("高度图: " + java.util.Arrays.toString(height));
int[] water = new int[n];
for (int i = 0; i < n; i++) {
water[i] = Math.max(0, Math.min(leftMax[i], rightMax[i]) - height[i]);
}
System.out.println("接水量: " + java.util.Arrays.toString(water));
}
public static void main(String[] args) {
TrappingRainWater solution = new TrappingRainWater();
int[] height = {0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1};
System.out.println("=== 接雨水问题 ===");
solution.visualize(height);
System.out.println("\nDP 解法: " + solution.trapDP(height.clone()));
System.out.println("双指针解法: " + solution.trapTwoPointers(height.clone()));
System.out.println("单调栈解法: " + solution.trapMonotonicStack(height.clone()));
// 测试三维接雨水
int[][] heightMap = {
{1, 4, 3, 1, 3, 2},
{3, 2, 1, 3, 2, 4},
{2, 3, 3, 2, 3, 1}
};
System.out.println("\n接雨水 II (三维): " + solution.trapRainWaterII(heightMap));
}
}
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 核心思想 |
|---|---|---|---|
| 动态规划 | O(n) | O(n) | 预处理最值,直观易懂 |
| 双指针 | O(n) | O(1) | 贪心策略,最优空间 |
| 单调栈 | O(n) | O(n) | 一次遍历计算多个凹槽 |
| 优先队列(II) | O(mn × log(m+n)) | O(mn) | 三维扩展,边界收缩 |
扩展思考
- 最大蓄水量:给定
n根柱子,选择两根作为边界,使它们之间的蓄水量最大(双指针经典题)。 - 接雨水 II:二维地形接雨水,需要使用优先队列从边界向内部渗透。
- 动态添加柱子:如果柱子高度可以动态变化,需要维护数据结构快速查询。
总结
接雨水问题是理解双指针贪心策略的绝佳案例。双指针解法的精妙之处在于:每次移动较短的一边,因为接水量由较短的边界决定。单调栈版本则展示了如何用栈结构处理”寻找下一个更大元素”类问题。从二维到三维的扩展,更体现了算法思维在不同维度间的迁移能力。