42. 接雨水
目录:
题目
给定 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
第一种方法:动态规划(使用额外的数组存储临时数据)
原理:一个位置处能存储的水的多少与这个位置左、右边的最大值中的最小值有关(木桶原理),如果这个最小值大于当前的高度,则当前位置能存储的雨水为这个高度差。
基于动态规划Dynamic Programming的,算法步骤:
- 我们维护两个个一维的dp数组,这个DP算法需要遍历两遍数组,
- 第一遍遍历,存入i位置左边的最大值到leftMax中,
- 第二遍遍历数组,存入i位置右边的最大值到rightMax中,
- 然后找出左右两边最大值中的较小值,然后跟当前值height[i]相比,如果大于当前值,则将差值叠加到结果
class Solution { public int trap(int[] height) { int n = height.length; if(n < 3) { return 0; } int[] leftMax = new int[n]; int[] rightMax = new int[n]; for(int i = 1;i<n;i++) { leftMax[i] = Math.max(leftMax[i-1],height[i-1]); } for(int i = n-2;i>=0;i--) { rightMax[i] = Math.max(rightMax[i+1],height[i+1]); } int ret = 0; for(int i = 1;i<n-1;i++) { int cur = Math.min(leftMax[i],rightMax[i])-height[i]; if(cur>0) { ret += cur; } } return ret; } }
第二种方法:改进的动态规划(使用变量存储临时中间数据)
基于上面每一个位置能否存储雨水的原理,我们不必额外的开辟数组存储两边的最大值信息,我们可以先找出数组中的最大值,这样一个位置一侧的最大值就确定(固定)住了,另一侧的最大值,根据上面动态规划中的递推公式,只需要一个变量进行记录更新就行了
class Solution { public int trap(int[] height) { int n = height.length; if(n < 3) { return 0; } int theMax = height[0]; int maxIndex = 0; for(int i = 1;i<n;i++){ if(height[i]>height[maxIndex]){ theMax = height[i]; maxIndex = i; } } int ret = 0; int leftMax = 0; for(int i = 1;i<maxIndex;i++){ leftMax = Math.max(leftMax,height[i-1]); int temp = Math.min(leftMax,theMax)-height[i]; if(temp>0){ ret+=temp; } } int rightMax = 0; for(int i = n-2;i>maxIndex;i--){ rightMax = Math.max(rightMax,height[i+1]); int temp = Math.min(rightMax,theMax)-height[i]; if(temp>0){ ret+=temp; } } return ret; } }
第三种方法:双指针排除移动扫描法
对于位置index,
- 如果能确定这个位置处的储水情况,就可以将指针越过这个位置
- 不能储水,直接跳过
- 能储水,计算此位置处的储水量后,移动跳过
- 对于可能存在的潜在的能储水的左右两个位置,现在还不能肯定的下结论,就只能保持不动
总之,我们是使用两个指针从两边向中间扫,
- 一定不会发生遗漏的需要判断的位置
- 能肯定的下结论的,移动这个指针
- 目前情况下,不能下结论的,指针保持不动
- 彼此间相互判断
实现1
class Solution { public int trap(int[] height) { int n = height.length; if(n < 3) { return 0; } int ret = 0; int leftMax = 0,rightMax = 0; int i = 0,j = n-1; while(i<=j) { //此时i位置处不可能能储水,故i位置处可以排除,移动跳过 while(i<=j && height[i]>=leftMax) { leftMax = height[i]; i++; } if(i>j) return ret; //此时j位置处不可能能储水,故j位置处可以排除,移动跳过 while(j>=i && height[j]>=rightMax) { rightMax = height[j]; j--; } if(j<i) return ret; //此时i,j处如果能储水单侧所需要满足的条件都已经具备,即: //leftMax>height[i],height[j]<rightMax else { //height[i] < leftMax < rightMax //此时能够确定i处左右两侧的最大值中的最小者 if(leftMax<rightMax) { ret+=leftMax-height[i]; i++; } else { //leftMax >= rightMax > height[j] //此时能够确定j处左右两侧最大值中的最小者 ret+=rightMax-height[j]; j--; } } } return ret; } }
实现2
class Solution { public int trap(int[] height) { int n = height.length; if(n<3)return 0; int ret = 0; //上一次的leftMax,rightMax初始化值 int leftMax = 0,rightMax = 0; //本轮即将判断的位置对 int i = 0,j = n-1; while(i<=j) { //先更新一下本轮需判断的位置对i,j的leftMax,rightMax值 //之所以在这个位置更新,是为了防止index超越数组下标范围 //leftMax含义:数组[0,i]范围内的最大值 //rightMax含义:数组[j,n-1]范围内的最大值 leftMax = Math.max(leftMax,height[i]); rightMax = Math.max(rightMax,height[j]); if(leftMax<=rightMax) { //如果此时i处的值大于它左边的最大值,则i处不能储水 //而根据leftMax值的更新公式,此时的leftMax等于height[i] //于是i处的储水值等于自己减自己,等于0,虽然i处不能储水,但是 //将它的储水值设定为0增加到ret上,不影响最后的结果 ret+=leftMax-height[i]; i++; } else { ret+=rightMax-height[j]; j--; } } return ret; } }
第四种方法:单调栈
- 栈中保存的元素非递增,作为潜在的卡槽的左挡板,由于栈中元素非递增,相当于卡槽能储水左边需满足的条件自然满足
- 从左到右遍历数组,如果当前元素大于栈顶元素,则当前元素能够作为卡槽的右挡板
- 这个算法是一层一层地计算存储的雨水量的
- 用例子模拟整个过程就可以写出完整的代码(不要技巧化,根据例子直接模拟写代码)
- 栈里保存的是下标,这样我们不丢失由于栈中元素的弹出而导致损失的位置信息
class Solution { public int trap(int[] height) { int n = height.length; if(n <3) return 0; int ret = 0; Deque<Integer> s = new LinkedList<>(); int i = 0; while(i < n) { if(s.isEmpty() || height[i] <= height[s.peek()]) { s.push(i); i++; } else { int index = s.peek(); //作为卡槽的底部 s.pop(); if(s.isEmpty()) //卡槽能储水,至少需要三个元素,左挡板,底部,右挡板 { s.push(i); i++; } else ret += (Math.min(height[i], height[s.peek()]) - height[index])*(i-s.peek()-1); } } return ret; } }