文章目录

42.接雨水

题目描述

给定​​n​​​个非负整数表示每个宽度为​​1​​的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水

思路

依次看每根柱子能接多少雨水。
某根柱子能接的雨水,取决于该柱子左侧的最高柱子​​​leftMax​​​,和右侧的最高柱子​​rightMax​​​。根据木桶效应,能接的水取决于左右两侧最高柱子的较小者,即​​h = min(leftMax,rightMax)​​​,若​​h​​比当前柱子大,则当前柱子能接到的雨水。那么一个比较直观的思路就是,我们先预处理出每个位置的左侧最大值和右侧最大值,然后再遍历一次,在每个柱子的位置接雨水即可。

class Solution {
public int trap(int[] height) {
int n = height.length;
int[] leftMax = new int[n];
int[] rightMax = new int[n];
leftMax[0] = height[0];
rightMax[n - 1] = height[n - 1];

for (int i = 1; i < n; i++) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}

for (int i = n - 2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}

int ans = 0;
for (int i = 0; i < n; i++) {
int h = Math.min(leftMax[i], rightMax[i]);
if (h > height[i]) ans += h - height[i];
}
return ans;
}
}

上面两次预处理,加一次遍历计算,总共花费的时间为​​3n​​​,其实可以优化为​​2n​​​,我们可以先预处理出​​rightMax​​​,然后,再边维护​​leftMax​​,边接雨水。

class Solution {
public int trap(int[] height) {
int n = height.length;
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]);
}

int ans = 0, leftMax = height[0];
for (int i = 1; i < n; i++) {
leftMax = Math.max(leftMax, height[i]);
int h = Math.min(leftMax, rightMax[i]);
if (h > height[i]) ans += h - height[i];
}
return ans;
}
}

实际上,我们还可以用双指针来进行优化,只需要遍历一次即可算出答案。

核心的思路是:用2个指针,​​i​​​从左往右移动,​​j​​​从右往左移动。我们能够在过程中正确维护​​i​​​的左侧最大值,和​​j​​​的右侧最大值,然后我们每次计算在​​i​​​或者​​j​​处的接雨水的量。

我们用​​iLeftMax​​​表示位置​​i​​​处左侧的最高柱子高度,​​iRightMax​​​表示​​i​​​右侧最高的柱子高度。​​jLeftMax​​​和​​jRightMax​​同理。

我们在双指针的遍历过程中,能够得到​​iLeftMax​​​和​​jRightMax​​的值。

由于​​i​​​在​​j​​​的左侧,我们容易得知如下关系:​​iLeftMax <= jLeftMax​​​。因为​​j​​​左侧的最大值,要么是​​i​​​左侧的最大值,要么是​​i​​​和​​j​​​之间出现了比​​iLeftMax​​更大的值。

同样,容易得到:​​iRightMax >= jRightMax​​​,因为​​i​​​比​​j​​​要多考虑区间​​[i,j]​​​,​​iRightMax​​​只可能比​​jRightMax​​更大。

整理一下,我们有:

iLeftMax <= jLeftMax
iRightMax >= jRightMax

并且我们能获取到​​iLeftMax​​​和​​jRightMax​​的值。

当​​iLeftMax <= jRightMax​​​时,根据上面的关系,有​​iLeftMax <= jRightMax <= iRightMax​​​,进而有​​iLeftMax <= iRightMax​​​,此时位置​​i​​​能接的雨水,只取决于​​iLeftMax​​​,而​​iLeftMax​​​我们是已知的,所以此时可以计算​​i​​处的雨水。

当​​iLeftMax > jRightMax​​​时,根据上面的关系,有​​jLeftMax >= iLeftMax > jRightMax​​​,进而有​​jLeftMax > jRightMax​​​,此时位置​​j​​​能接的雨水,只取决于​​jRightMax​​​,而​​jRightMax​​​我们是已知的,所以此时可以计算​​j​​处的雨水。

双指针优化后,只需要一趟遍历即可完成计算。

class Solution {
public int trap(int[] height) {
int n = height.length;
int leftMax = height[0], rightMax = height[n - 1];
int i = 1, j = n - 2;
// iLeftMax <= jLeftMax
// iRightMax >= jRightMax
int ans = 0;
while (i <= j) {
if (leftMax <= rightMax) {
// iLeftMax <= jRightMax <= iRightMax
leftMax = Math.max(leftMax, height[i]);
ans += leftMax - height[i];
i++;
} else {
// jLeftMax >= iLeftMax > jRightMax
rightMax = Math.max(rightMax, height[j]);
ans += rightMax - height[j];
j--;
}
}
return ans;
}
}

其余解法:单调栈。

换个角度考虑,什么时候某个位置可能接到雨水呢,是不是只有柱子高度先下降,后上升,形成一个 “盆地”时(一个V字形),在中间的低洼处才能接到水呢?于是我们考虑用单调栈。栈里面存的是高度递减的柱子,从左往右遍历的时候,只要遇到一个比栈顶的柱子高度大的,则说明能和前面的柱子形成一个低洼处,能够接到雨水。

class Solution {
public int trap(int[] height) {
int n = height.length;
int[] stack = new int[n];
int top = -1; // 栈顶
int ans = 0;

for (int i = 0; i < n; i++) {
// 栈非空, 且栈顶的柱子高度小于当前柱子, 则开始接雨水
while (top >= 0 && height[stack[top]] < height[i]) {
int j = stack[top--]; // 准备接j这个位置的雨水
if (top < 0) break; // 没有前一个柱子了, 无法接j处的雨水
int k = stack[top]; // k和i围住了j, 可以接j处雨水, 注意不要把k也弹出栈
int minH = Math.min(height[k], height[i]);
ans += (minH - height[j]) * (i - k - 1);
}
// 该柱子能和前面的柱子接的雨水, 计算完毕, 插入该柱子
stack[++top] = i;
}
return ans;
}
}

407.接雨水II

题目描述:

给你一个 ​​m x n​​ 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。

上一道题是一维的,这道题是二维的。

思路

第一次见这道题时,不太容易想清楚 [满足什么条件才能接到雨水] 这一点。比较直觉但是错误的想法是,对于某个位置​​[i,j]​​,找到其上下左右4个方向上,高度最大的4根柱子,取4根柱子中高度最小的,再和当前这跟柱子比较高度。

这样是错误的,水还可能从其他边边角角的地方流走。一种相对正确但是不太好想象的想法是:对于​​[i,j]​​​,找到其四周围成一圈的一堵墙,这堵墙中,高度最小的柱子,再和​​[i,j]​​的柱子比高度。但这种想法很难付诸实践。

我们换个角度来看,当下完雨后,每个柱子有个最终高度,若这个最终高度大于该柱子原有高度,则说明在这个柱子的位置,接到了雨水。我们用​​f(i,j)​​​来表示在位置​​[i,j]​​处的柱子的最终高度。引入这个最终高度的概念后,就很好想了。

​f(i,j)​​,其实就取决于其周围4个相邻位置的最终高度。容易得到状态转移方程如下:

​f(i,j) = max{ min{f(i-1, j), f(i + 1, j), f(i, j - 1), f(i, j + 1)}, height[i][j] }​

即,位置​​[i,j]​​​的柱子的最终高度,为其4个相邻位置的最终高度的最小值,再和​​[i,j]​​柱子原本的高度取一个max。

然后是状态的边界。容易得知,整个矩形最外层的柱子,都是接不到水的,水一定会从边界流出去。所以对所有最外层的柱子,初始化其​​f(i,j) = height[i][j]​​​。我们每次,从已经求得​​f(i,j)​​​的柱子中,找到​​f(i,j)​​​值最小的,并更新其相邻的,还未求出​​f(i,j)​​的柱子。

为什么这样能求出正确答案。首先,根据木桶效应,​​f(i,j)​​​只取决于其相邻的4个位置的最小者。所以我们每次从已经确定​​f(i,j)​​​的柱子中,找出值最小的。假设我们选中的​​f(i,j)​​​最小的柱子是a。那么, 与柱子a相邻的柱子,其​​f(i,j)​​一定是由柱子a转移过来的吗?

答案是肯定的,假设与柱子a相邻的某个柱子b,其还有其他3个相邻的柱子,若这3个相邻的柱子的最终高度已经求出,而我们选择的最小值为柱子a,说明这3个相邻柱子的最终高度都大于柱子a的。若这3个相邻的柱子里,有些柱子的最终高度还没有求出,则这些还没有求出最终高度的柱子,其最终高度,都是由已经求出最终高度的某根柱子转移过来的,而状态转移的过程中,​​f(i,j)​​只可能增大,不可能变小,所以这些没有求出最终高度的柱子的最终高度,一定是大于其状态转移链的初始那个状态,而这个初始状态一定是此时​​f(i,j)​​​已知的,而此时已知的​​f(i,j)​​中,柱子a是最小的。也能够推出,其他3个相邻柱子的最终高度都一定大于a。所以柱子b的最终高度,一定是由柱子a的状态更新过来的。

再换个角度想,我们将最外层的柱子的状态​​f(i,j)​​进行了初始化,相当于在最外圈,围起了一堵墙(边界),这堵墙中间的某根柱子,是否能接到水,一定取决于最外圈这堵墙中(边界上),高度最小的那根柱子。所以我们每次需要取高度最小的。

于是,我们的算法就出来了。其实和Dijkstra特别像。每次从​​f(i,j)​​已知的柱子中,选择一个最小的,更新其相邻的柱子。我们用小根堆来做。

class Solution {

class Node {
int x;
int y;
int h; // 下雨之后该柱子的最终高度

Node(int x, int y, int h) {
this.x = x;
this.y = y;
this.h = h;
}
}

public int trapRainWater(int[][] heightMap) {
int n = heightMap.length, m = heightMap[0].length;
boolean[][] st = new boolean[n][m]; // 用来标记那些已经算出最终高度的柱子
// 按最终高度从小到大
PriorityQueue<Node> heap = new PriorityQueue<>((o1, o2) -> o1.h - o2.h);
// 将矩阵最外层的柱子插进堆
for (int i = 0; i < m; i++) {
heap.offer(new Node(0, i, heightMap[0][i])); // 第一行
heap.offer(new Node(n - 1, i, heightMap[n - 1][i])); // 最后一行
st[0][i] = st[n - 1][i] = true;
}
// 重复元素不影响最终答案
for (int i = 0; i < n; i++) {
heap.offer(new Node(i, 0, heightMap[i][0])); // 第一列
heap.offer(new Node(i, m - 1, heightMap[i][m - 1])); // 最后一列
st[i][0] = st[i][m - 1] = true;
}
int ans = 0;

int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};

while (!heap.isEmpty()) {
Node node = heap.poll();
for (int i = 0; i < 4; i++) {
int nx = node.x + dx[i];
int ny = node.y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m || st[nx][ny]) continue; // 越界或已求出最终高度
ans += Math.max(node.h, heightMap[nx][ny]) - heightMap[nx][ny]; // 接雨水
heap.offer(new Node(nx, ny, Math.max(node.h, heightMap[nx][ny])));
st[nx][ny] = true; // 该柱子已求出最终高度
}
}
return ans;
}
}

上面的思路是我自己看了yxc的视频讲解后,总结的一种比较简单,方便记忆和理解的思路。
yxc的原思路​。其证明和推导过程十分严谨,但不是特别容易理解。