42. 接雨水 + 动态规划 + 双指针
题目来源
LeetCode_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 * 104
0 <= height[i] <= 105
题解分析
解法一:暴力法
- 暴力法比较简单,但是需要一些思考(结合图中柱子的特点)。
- 分别找出当前柱子左边和右边的最大值,然后取这两个值的最小值作为边界(利用短板原理)。
class Solution {
public int trap(int[] height) {
//从左到右依次考虑每个柱子
int n = height.length;
int sumWater = 0;
for(int i=0; i<n; i++){
//从右往左找到最高的一个柱子的高度
int maxLeft = height[i], maxRight = height[i];
for(int j=i; j>=0; j--){
maxLeft = Math.max(maxLeft, height[j]);
}
for(int j=i; j<n; j++){
maxRight = Math.max(maxRight, height[j]);
}
int h = Math.min(maxLeft, maxRight) - height[i];
sumWater += h;
}
return sumWater;
}
}
复杂度分析
- 时间复杂度: O(n^2)。数组中的每个元素都需要向左向右扫描。
- 空间复杂度: O(1) 的额外空间。
解法二:方法一优化,动态编程
- 可以使用空间换时间,提前使用两个数字来存储当前柱子左右的最大高度
- 在循环时即可直接索引数组中的值
class Solution {
public int trap(int[] height) {
if(height == null || height.length == 0)
return 0;
//从左到右依次考虑每个柱子
int n = height.length;
int sumWater = 0;
int[] maxLeft = new int[n];
int[] maxRight = new int[n];
maxLeft[0] = height[0];
for(int i=1; i<n; i++){
maxLeft[i] = Math.max(maxLeft[i-1], height[i]);
}
maxRight[n-1] = height[n-1];
for(int i=n-2; i>=0; i--){
maxRight[i] = Math.max(maxRight[i+1], height[i]);
}
for(int i=1; i<n-1; i++){//注意这里的循环边界
int h = Math.min(maxLeft[i], maxRight[i]) - height[i];
sumWater += h;
}
return sumWater;
}
}
解法三:双指针法
- 和方法 2 相比,我们不从左和从右分开计算,我们想办法一次完成遍历。
- 从动态编程方法的示意图中我们注意到,只要 \(right_{max}[i]>left_{max}[i]\) (元素 0 到元素 6),积水高度将由 left_max 决定,类似地 \(left_{max}[i]>right_{max}[i]\)(元素 8 到元素 11)。
- 所以我们可以认为如果一端有更高的条形块(例如右端),积水的高度依赖于当前方向的高度(从左到右)。当我们发现另一侧(右侧)的条形块高度不是最高的,我们则开始从相反的方向遍历(从右到左)。
- 我们必须在遍历时维护 \(left_{max}\) 和 \(right_{max}\) ,但是我们现在可以使用两个指针交替进行,实现 1 次遍历即可完成。
class Solution {
public int trap(int[] height) {
int n = height.length;
int left = 0, right = n-1;
int lmax = 0, rmax = 0;
int res = 0;
while(left < right){
lmax = Math.max(lmax, height[left]);
rmax = Math.max(rmax, height[right]);
if(lmax < rmax){
res += (lmax - height[left]);
left++;
}else{
res += (rmax - height[right]);
right--;
}
}
return res;
}
}
方法四:单调栈
- 除了使用双指针法,本题也可以使用单调栈来解决,因为当依次从左到右考虑时,可以根据一定的规律不断计算出一部分的答案。
- 那这种特殊的规律是什么呢?换句话说,当遇到什么情况可以计算出一部分的答案呢?其实仔细看样例,我们可以发现,每次遇到当前高度比前一个高度高时就可以计算出低洼处的面积。
- 所以,为了可以每次不断计算低洼处的面积,我们维护一个单调递减栈,每次都保存栈是单调递减的(只有是单调递减的,我们才能当遇到一个更高的柱子时不断计算前面的低洼处)。
- 需要注意的是,我们的单调栈中存放的不是高度,而是索引,这点容易被忽略。之所以存储索引,是因为我们需要计算面积,通过索引可以计算宽度。
class Solution {
public int trap(int[] height) {
int n = height.length;
int res = 0;
LinkedList<Integer> sta = new LinkedList<>();
for(int i=0; i<n; i++){
while(!sta.isEmpty() && height[i] > height[sta.peekFirst()]){
int cur = sta.pollFirst();
if(sta.isEmpty()){
break;
}
int curh = height[cur];
int left = sta.peekFirst();
int lefth = height[left];
int righth = height[i];
res += (Math.min(righth, lefth) - curh) * (i - left - 1);
}
sta.addFirst(i);
}
return res;
}
}
参考
Either Excellent or Rusty