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]相比,如果大于当前值,则将差值叠加到结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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; } } |
第二种方法:改进的动态规划(使用变量存储临时中间数据)
基于上面每一个位置能否存储雨水的原理,我们不必额外的开辟数组存储两边的最大值信息,我们可以先找出数组中的最大值,这样一个位置一侧的最大值就确定(固定)住了,另一侧的最大值,根据上面动态规划中的递推公式,只需要一个变量进行记录更新就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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; } } |
第四种方法:单调栈
- 栈中保存的元素非递增,作为潜在的卡槽的左挡板,由于栈中元素非递增,相当于卡槽能储水左边需满足的条件自然满足
- 从左到右遍历数组,如果当前元素大于栈顶元素,则当前元素能够作为卡槽的右挡板
- 这个算法是一层一层地计算存储的雨水量的
- 用例子模拟整个过程就可以写出完整的代码(不要技巧化,根据例子直接模拟写代码)
- 栈里保存的是下标,这样我们不丢失由于栈中元素的弹出而导致损失的位置信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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; } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
2021-06-02 transformers中,关于PreTrainedTokenizer的使用