文章目录
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的原思路。其证明和推导过程十分严谨,但不是特别容易理解。