42. 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。
示例:
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
解题思路
思路1
- 从左至右遍历,指针
i
- 指针
j
从i
往后的位置遍历- 查找第一个高度大于等于
heigh[i]
的位置maxBiggerIndex
,找到后记录[i,maxBiggerIndex]
的纳雨量,退出此循环,指针i
移动到maxBiggerIndex
的位置进行下次遍历 - 若不存在一个位置的高度比他高,则查找高度比其小的位置中最高的那一位的位置
maxSmallerIndex
- 查找第一个高度大于等于
- 若遍历完仍未找到
maxBiggerIndex
,则记录[i,maxSmallerIndex]
的纳雨量,指针i
移动到maxSmallerIndex
的位置进项下次遍历
- 指针
- 每次记录的纳雨量之和即为总纳雨量
- NOTE
- 刚开始有过两种错误思路:
- 认为找到第一个大于等于
height[i]
的高度的位置j
,记录其之间的纳雨量即可,没考虑到j
的高度小于i
的高度时,也可容纳降雨量,如测试用例[4,2,3]
- 认为只要找到所有的极大值点,然后计算相邻的极大值点之间的纳雨量再求和即可,没考虑到
(极大值1,极大值2,极大值3)
,其中极大值1
和极大值3
均大于极大值2
的情况,这种时候应该计算[极大值点1,极大值点3]
的纳雨量而不是[极大值点1,极大值点2]和[极大值点2,极大值点3]之和
,如测试用例[7,4,0,4,2,4,4,5]
- 以上两种错误思路均是由于测试用例过少得出的片面结论,当多次测试检测出错误以后,就能慢慢改正得到正确的解题思路,不过要费一些时间
思路2
- 某一点
i
处可容纳的降水量等于其左侧的最大高度和其右侧的最大高度中的较小值与点i处的高度的差值,对每一点都如此计算纳雨量再求和,时间复杂度o(n2),空间复杂度o(1)- 优化①(牺牲空间换取时间):打表记录每一点处的左侧最大高度和右侧最大高度,这样计算纳雨量的时候只需要查表即可,渐进时间复杂度O(n),空间复杂度O(n)
- 优化②:双指针法,不许牺牲空间。时间复杂度O(n),空间复杂度O(1)
- 解题思路如下:参考:接雨水-题解
我们先明确几个变量的意思:left_max:左边的最大值,它是从左往右遍历找到的 right_max:右边的最大值,它是从右往左遍历找到的 left:从左往右处理的当前下标 right:从右往左处理的当前下标
- 解题思路如下:参考:接雨水-题解
-
在某个位置i处,它能存的水,取决于它左右两边的最大值中较小的一个。
-
当我们从左往右处理到left下标时,左边的最大值left_max对它而言是可信的,但right_max对它而言是不可信的。(见下图,由于中间状况未知,对于left下标而言,right_max未必就是它右边最大的值)
-
当我们从右往左处理到right下标时,右边的最大值right_max对它而言是可信的,但left_max对它而言是不可信的。
-
对于位置left而言,它左边最大值一定是left_max,右边最大值“大于等于”right_max,这时候,如果left_max<right_max成立,那么它就知道自己能存多少水了。无论右边将来会不会出现更大的right_max,都不影响这个结果。 所以当left_max<right_max时,我们就希望去处理left下标,反之,我们希望去处理right下标
代码
法1
class Solution {
public:
//计算[i,j]可接纳的降水量
int compute(vector<int>& height, int i, int j){
int ans = 0;
int maxHeight = min(height[i],height[j]);
for(int k = i+1; k < j; k++){
ans += (maxHeight-height[k]);
}
return ans<0 ? 0 : ans;
}
int trap(vector<int>& height) {
if(height.size()<3)
return 0;
int n = height.size();
int ans = 0;
for(int i = 0; i < n; i++){
int maxBiggerIndex = i;//第一个大于等于height[i]的位置
int maxSmallerIndex = i;//小于height[i]的所有值中的最大值的位置
int maxSmaller = 0;//小于height[i]的所有值中的最大值
for(int j = i + 1; j < n; j++){
if(maxBiggerIndex==i){//尚未找到maxBigger
if(height[j]>=height[i]){//找到maxBigger
maxBiggerIndex = j;
}
if(height[j]>maxSmaller){//找到maxSmaller
maxSmallerIndex = j;
maxSmaller = height[j];
}
}
if(maxBiggerIndex!=i) {//已找到maxBigger
ans += compute(height,i,maxBiggerIndex);
i = maxBiggerIndex-1;
break;
}
}
//未找到maxBiggerIndex但找到maxSmaller
if(maxBiggerIndex==i&&maxSmallerIndex!=i){
ans += compute(height,i,maxSmallerIndex);
i=maxSmallerIndex-1;
}
}
return ans;
}
};
法2优化①:打表
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
if(n<3)
return 0;
vector<int> leftMax(n,0);
vector<int> rightMax(n,0);
//计算左侧最大值
leftMax[0]=height[0];
int maxHeight=leftMax[0];
for(int i=1; i<n; i++){
maxHeight = max(maxHeight,height[i]);
leftMax[i] = maxHeight;
}
//计算右侧最大值
rightMax[n-1]=height[n-1];
maxHeight=rightMax[n-1];
for(int i=n-2; i>=0; i--){
maxHeight = max(maxHeight,height[i]);
rightMax[i] = maxHeight;
}
//遍历计算纳雨量
int ans = 0;
for(int i=0; i<n; i++){
maxHeight = min(leftMax[i],rightMax[i]);
ans += (maxHeight - height[i]);
}
return ans;
}
};
法2优化②:双指针
class Solution {
public:
//双指针法
int trap(vector<int>& height) {
int n = height.size();
if(n<3)
return 0;
int left = 0;
int right = n - 1;
int left_max = 0;
int right_max = 0;
int ans = 0;
while(left <= right){
left_max = max(height[left],left_max);
right_max = max(height[right],right_max);
if(left_max<=right_max){
ans += left_max-height[left];
left++;
}
else {
ans += right_max-height[right];
right--;
}
}
return ans;
}
};