1 题目描述(LeetCode42)
给定n个非负正数表示每个宽为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例如下:

2 题目解决
解决此类问题的方法多种多样,这里我主要使用了三种比较方法,一种是动态规划法,也是最好理解最好想的办法,一种是双指针法,另一种是将雨水看成整体法。下面依次进行介绍。
2.1 动态规划法
要想求接雨水的最大量,我们看每个柱子上能接多少雨水。只有该柱子左右两边有比自己高的柱子时才能接到水,而接雨水类似于木桶效应,一个柱子上能接的最大雨水为其左右两边最高柱子的较矮的柱子减去当前柱子的高度,如下图:

所以一个很自然的想法是,使用两个数组lmax[]和rmax[]保存每一个height[i]的左右两边最高的柱子。那么如何求最高的柱子呢?以求height[i]左边的最高柱子为例,可以发现其与lmax[i-1]的关系,因为在求解lmax[i-1]时已经将nums[i-1]与其左边的数进行了比较,因此求lmax[i]时只需比较height[i]与lmax[i-1]谁更大即可。即lmax[i]=max{lmax[i-1],height[i]}。同理rmax[i]=max{rmax[i+1],height[i]}。最后柱子height[i]所接的水为min{lmax[i],rmax[i]-height[i]}。
由于最左边和最右边的柱子一定不能接水,所以lmax[0]=height[0],rmax[n-1]=right[n-1]。最终代码如下:
class Solution {
public:
int trap(vector<int>& height) {
int n=height.size();
int ans=0;
vector<int> lmax(n);
vector<int> rmax(n);
lmax[0]=height[0];
rmax[n-1]=height[n-1];
//求每个柱子i左边最高的柱子
for(int i=1;i<n;i++){
lmax[i]=max(lmax[i-1],height[i]);
}
//求每个柱子i右边最高的柱子
for(int i=n-2;i>=0;i--){
rmax[i]=max(rmax[i+1],height[i]);
}
for(int i=0;i<n;i++){
ans+=min(lmax[i],rmax[i])-height[i];
}
return ans;
}
};
这样时间复杂度为 O ( n ) O(n) O(n),但空间复杂度也为 O ( n ) O(n) O(n)。下面介绍一个更优的算法。
2.2 双指针法
双指针法类似于动态规划的思路,只不过不需要知道左右两边具体的最大值。由于最左边和最右边的柱子都不可能接水,因此定义左右两个指针l和r分别从两边遍历。我们不用数组存储最大值,而是使用lmax和rmax来记录遍历过程中左边的最大值和右边的最大值。类似地,lmax=max{lmax,height[i]},rmax=max{rmax,height[i]}。
为什么说我们不需要具体知道左右两边的最大值。因为我们在移动l时,l左边的最大值是已知的,同理移动r时,r右边的最大值也是已知的。如何将其联系起来是关键。若我们每次移动指向较小高度柱子的指针,下面说明,当height[l]<height[r],则此时的lamx一定小于rmax。
我们从最初状态开始,假设最初移动的是左指针,则只有当左指针移动到比右指针指向的柱子高时,右指针才会第一次移动。然后右指针指向更高的柱子,左指针才再次移动。因此当左指针移动时,lmax<rmax,右指针移动时,rmax<=lmax。
也就是说,我们在移动左右指针的过程中,间接的与当前元素的右左最高值进行了比较。即左移时,我们已经知道height[l]左边的最高值为lmax,且lmax<rmax,而rmax不一定是height[l]右边的最高柱子,但height[l]右边最高柱子一定比rmax高,因此我们不需要知道其右边最高柱子的具体值。同理右指针移动也是如此。所以减少了空间开销。
如果实在不好理解,则考虑极端情况,即左指针一开始就指向的是所有柱子中最高的那个,则全程只有右指针移动。
双指针算法的时间复杂度为
O
(
n
)
O(n)
O(n),但空间复杂度仅为
O
(
1
)
O(1)
O(1)。但是若没接触过,实在难以想到且不太好理解。下面是实现代码:
class Solution {
public:
int trap(vector<int>& height) {
int n=height.size();
int ans=0;
int l=0,r=n-1;
int lmax=0,rmax=0;
while(l<r){
lmax=max(lmax,height[l]);
rmax=max(rmax,height[r]);
if(height[l]<height[r]){
// 左低右高,左位置接水量由左最大高度决定
ans+=(lmax-height[l]);
l++;
}
else{
//相反
ans+=(rmax-height[r]);
r--;
}
}
return ans;
}
};
2.3 整体法
即直接将最终接的水也看成柱子,然后算出每一层的面积,累加起来减去柱子的面积即为答案,如图:

这样我们只需要找到最高的柱子,然后依次算每一层的面积,算每一层的面积也很简单,只需要从高度1依次循环,每次找到左右两边第一个等于该高度的柱子,二者之间的距离就是该层的面积,最终将每一层面积累加减去柱子面积即可。实现代码如下:
class Solution {
public:
int trap(vector<int>& height) {
//找到最高的柱子
int maxheight=*max_element(height.begin(),height.end());
int n=height.size();
int ans=0;
//循环每一层
for(int i=1;i<=maxheight;i++){
int l=0,r=n-1;
while(height[l]<i&&l<r){
l++;
}
while(height[r]<i&&l<r){
r--;
}
ans+=(r-l+1);
}
ans-=accumulate(height.begin(),height.end(),0);
return ans;
}
};
很好理解但不太好想到的办法。其空间复杂度为 O ( 1 ) O(1) O(1),但是其时间复杂度与柱子的高度有关,就运行时间来看,应该是介于 O ( n ) O(n) O(n)与 O ( n 2 ) O(n^2) O(n2)之间的。
3 结语
今天课不少而且还在忙别的,只写了这一题,理解了三种算法。接下来打算按照算法类别进行刷题,但感觉效率有太低。但这样随机刷题,又太没针对性。哎,算法路漫漫,兴则降至。加油。

2250

被折叠的 条评论
为什么被折叠?



