1、题目描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
提示:
n == height.length
1 <= n <= 2 * 10^4
0 <= height[i] <= 10^5
2、思路及代码
2.1 按列计算的思路
核心的思路:雨水的计算是按列计算,也就是通过各种方法计算每一列能积攒的雨水,然后将所有列的雨水加和得到答案。而计算每一列能积攒的雨水的方法就是将本列左边的所有墙最大值和本列右边所有墙的最大值取个最小值,然后用这个最小值减去本列的高度,就可以得到本列能积攒的水。
所有的方法都是采用这个思路去处理,但是,不同的方法对于获取这个最小值是不同的。
2.1.1 暴力
所谓暴力的意思就是对于获取两边的最值这个过程不优化,就是计算到那一列直接进行求解。代码如下:
class Solution {
public:
int trap(vector<int>& height) {
int ans = 0;
for(int i = 0; i < height.size(); i ++)
{
// 这里初始化我是把lmax和rmax都设置成当前高度了,也就是min(lmax,rmax)最小也会是height[i]了,只有高于这个数值才会有积水。
int lidx = i,ridx = i;
int lmax = height[i],rmax = height[i];
while(lidx >= 0) lmax = max(lmax,height[lidx --]);
while(ridx < height.size()) rmax = max(rmax,height[ridx ++]);
ans += min(lmax,rmax) - height[i];
}
return ans;
}
};
时间复杂度来看,每个列都要计算n次获得两边的最值。时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度
O
(
1
)
O(1)
O(1)
2.1.2 预处理(动态规划)
每次都计算所有的就显得很呆。因为有太多显而易见的重复计算。所以我们想能不能把每个列的左右最大值通过两次遍历预处理出来。然后再计算。
class Solution {
public:
int trap(vector<int>& height) {
vector<int> lmax(height.size()),rmax(height.size());
int tmplmax = -1,tmprmax = -1;
//预处理
for(int i = 0; i < height.size(); i ++)
{
if(tmplmax < height[i]) tmplmax = height[i];
lmax[i] = tmplmax;
}
//预处理
// 注意这里要从后往前计算,因为要算本列右边的最大值
for(int i = height.size() - 1; i >= 0; i --)
{
if(tmprmax < height[i]) tmprmax = height[i];
rmax[i] = tmprmax;
}
int ans = 0;
for(int i = 0; i < height.size(); i ++)
{
ans += min(lmax[i],rmax[i]) - height[i];
}
return ans;
}
};
时间复杂度为 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n),空间换时间了属于。
2.1.3 双指针
其核心思想是,当知道左边(右边)的最大值是当前左右两边最大值的最小值时,我们无需也把右边(左边)的最大值也求出来。因为我们的目的就是求出当前列左右两边最大值中的最小值,在我们已经确定当前这个最大值是左右两边的最小值的时候,我们就不需要非要求出另一边的最大值就可以得到这一列的结果。
代码如下:
左边指针lidx = 0,右边指针ridx = n - 1,左边最大值为lmax,表示从0到lidx - 1中元素的最大值,右边最大值为rmax,表示从ridx + 1到n-1中元素的最大值。
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
int lmax = 0, rmax = 0;
int lidx = 0, ridx = n - 1;
int ans = 0;
while(lidx < ridx)
{
//判断当前lidx或者ridx中的哪一个的左右最大值的最小值能定下来(具体下面解释)
if(height[lidx] < height[ridx])
{
//确定lidx左边的最大值一定是小于右边的(虽然我并不知道lidx右边的最大值具体是多少,但是左边的一定小就对了)
//然后我们计算lidx这个列能积的水
//lmax是lidx左边所有元素的最大值,如果lidx的值大于它就不能积水
if(height[lidx] < lmax) ans += lmax - height[lidx];
else lmax = height[lidx];
lidx ++;
}else{
// 同理
if(height[ridx] < rmax) ans += rmax - height[ridx];
else rmax = height[ridx];
ridx --;
}
}
return ans;
}
};
我觉得这一步一开始不是很好理解
//判断当前lidx或者ridx中的哪一个的左右最大值的最小值能定下来
if(height[lidx] < height[ridx])
也就是为什么,假设h[lidx] < h[ridx],我们就能知道存在一个idx, 大于lidx,使得lmax < h[idx],从而lmax < Max(h[lidx + 1],h[lidx + 2] , …, h[n - 1],从而确定lidx的左边的最大值小于右边的最大值,h[lidx] > h[ridx]同理。
我们这样看。
一开始的时候,lidx = 0,ridx = n - 1,并且ridx始终在lidx的右边。假设h[lidx] < h[ridx],则有 lidx ++。直到h[lidx] > h[ridx]。在h[lidx] > h[ridx]之前,对于所有的从0 到lidx 的元素i,由于h[ridx] > i,并且ridx > lidx,故对于这些元素,我们就知道其左边的最大值一定小于右边。
当h[lidx] > h[ridx]时,我们有h[0],…, h[lidx - 1] < h[ridx]的而此时h[lidx] >= h[ridx],就有h[lidx] > h[0] ,…,h[lidx - 1],是已遍历元素的最大值。此时ridx 就开始 - -,直到h[lidx] 再次小于h[ridx],此时我们知道h[ridx + 1] 到 h[n - 1]都是小于 h[lidx]的。而h[ridx] > h[lidx],所以此时h[ridx]是已遍历所有元素的最大值。
由此我们知道,当h[lidx] < h[ridx] 的时候,h[ridx]是已遍历元素的最大值,此时对于h[lidx]来说,左侧的最大值lmax就是其左右最大值的最小值,可以用来更新答案。同理反之,h[lidx]就是已遍历元素的最大值,对于h[ridx]来说,右侧的最大值rmax就是其左右最大值的最小值,可以用来更新答案。
2.2 按行计算的思路
按行计算的思路,其核心就是以行为单位进行水坑的计算,也就是把水坑分成横条进行计算。
2.2.1 暴力
暴力的做法应该就是统计每个高度的水的数量,加和处理。
2.2.2 单调栈
使用栈进行处理,其核心就是只要遇到一个水坑右边的墙,就开始计算整个坑的水量。
代码如下:
class Solution {
public:
int trap(vector<int>& height) {
stack<int> s;
int ans = 0;
for(int i = 0; i < height.size(); i ++)
{
while(!s.empty() && height[i] > height[s.top()])
{
int tophight = height[s.top()];
s.pop();
if(s.empty()) break;
int lefthight = height[s.top()];
int righthight = height[i];
int hight = min(lefthight,righthight) - tophight;
int width = i - s.top() - 1;
ans += width * hight;
}
s.push(i);
}
return ans;
}
};