题目: 接雨水
描述:
给定 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 个单位的雨水(蓝色部分表示雨水)。
方法一:我们一共接到的雨水量等于每个柱子接到的雨水量,所以我们把求解一共接到的雨水量划分为求解每个柱子接到的雨水量这样的子问题,对于柱子i,它能够接到的最大雨水量,应该是取决于它左右两边最高的柱子中较小的一个(短板效应),然后再减去柱子i本身的高度,即为柱子i能够接到的雨水量,这样我们遍历数组,求得每个柱子能够接到的雨水量最后再求和。但是对于每一根柱子i,寻找它左右两边最高的柱子的时间复杂度为o(n),因而算法的总时间复杂度为o(n²),所以我们考虑优化寻找左右两边最高柱子的时间,我们可以采取空间换时间的做法,把每一根柱子左右两边最高的柱子用数组 leftMax存储下来,中间可利用动态规划,这里讲解左边柱子的最大值求解过程,显然第二根柱子左边最大的为第一根柱子,即为leftMax[1] = height[0],对于后面的柱子满足leftMax[i] = max(leftMax[i-1],height[i-1]),右边最大的柱子同理可求
时间复杂度:o(n)
空间复杂度:o(n)
int trap(vector<int>& height) {
int n = height.size();
vector<int> leftMax(n);//存储柱子i左边最高的柱子的高度
vector<int> rightMax(n);//存储柱子i右边最高的柱子的高度
if(n==2||n==1){//柱子数量为1或者2时候,接不到水
return 0;
}
leftMax[1] = height[0];
rightMax[n-2] = height[n-1];
for(int i=2;i<n;i++){
leftMax[i] = max(leftMax[i-1],height[i-1]);
}
for(int i = n-3;i>=0;i--){
rightMax[i] = max(rightMax[i+1],height[i+1]);
}
int sum = 0;
for(int i=1;i<n-1;i++){
int min = 0;
if(rightMax[i]<leftMax[i]){//接水量取决于短的柱子
min = rightMax[i];
}else{
min = leftMax[i];
}
if(min-height[i]>0){
sum+=min-height[i];
}
}
return sum;
}
方法二:双指针 我们方法一空间复杂度为o(n),现在我们改进方法来使得空间复杂度为o(1)
对于左边和右边的最大柱子,我们可以不用存储到数组里面,而是用两个变量leftMax和rightMax动态维护,我们定义两个指针left,right分别指向数组头部和尾部,然后我们通过这两个指针遍历数组,相遇时结束遍历。下面考虑我们指针移动的条件以及对于遍历元素的操作,
在此之前先说明,leetcode官方的双指针题解中的说法是有误的,下面图片中为有误的说法:
可以看到leetcode官方说如果height[left]<height[right],则必有leftMax<rightMax,我们知道leftMax>=height[left],rightMax>=height[right],所以仅通过height[left]<height[right]是无法判断leftMax和rightMax的大小的,比如我们假设数组为[7,4,5,2],假如left指针此时指向元素4,而right指针此时指向5,此时的leftMax
= 7,而rightMax = 5,则有leftMax>rightMax,这与上面说的leftMax<rightMax矛盾,所以leetcode原文中的说法是错误的。
但是为什么官方给出的代码是正确的呢?这里其实原文中代码的做法是正确的,因为用代码中的算法,不可能出现我上面举出例子里面left和right指针所指向的情况,但是官网题解的说法出现了错误,不能像图片那样子不严格的去描述大小关系,下面给出关于代码正确的说法。
我们从left从第一个元素开始起,right从最后一个元素开始,如果height[left]<right[right],而此时的rightMax = right[right],leftMax = height[left],此时第一个柱子蓄水量为0,然后left++,假如仍然有height[left]<height[right],假如leftMax更新,也满足leftMax<height[left]。现在考虑另外一种情况,假如height[left]>height[right],那么leftMax更新,而此时rightMax肯定也小于leftMax.综上所述,如果left指针在没有遇到比right大的元素时候,指针会一直右移,此时也恒满足leftMax<rightMax,而如果碰到比right大的元素,left指针停止移动,此时更新leftMax,则满足leftMax>rightMax,然后right指针继续重复left做的事情。需要注意的是我们的两个指针并不会同时移动,而是根据条件来判断哪个指针移动。
nt trap(vector<int>& height) {
int n = height.size();
int left = 0,right = n-1;
int leftMax = 0,rightMax = 0;
int sum = 0;
while(left<right){
leftMax = max(leftMax,height[left]);
rightMax = max(rightMax,height[right]);
if(height[left]<height[right]){
sum+=leftMax - height[left++];//rightMax更新,left开始移动
}else{
sum+=rightMax - height[right--];//leftMax更新,right开始移动
}
}
return sum;
}