让我们来切土豆
题目
给定 n 个非负整数表示每个宽度为 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 个单位的雨水(蓝色部分表示雨水
题目链接
https://leetcode-cn.com/problems/trapping-rain-water/
动态规划的思路就是纵方向来看,计算每个位置能容纳多少雨水。
每个位置能容纳雨水的量是由该位置左边最高点和右边最高点决定。
如图,位置 i 处所能容纳的雨水是由左右侧maxLeft和maxRight中的较小值决定。
所以需要对每个位置求出其左侧和右侧的最值。
利用动态规划来快速求值,位置 i 左侧的最值就是位置 i-1 左侧的最值和位置 i 自身的值进行比较。
m a x L e f t [ i ] = m a x ( m a x L e f t [ i − 1 ] , h e i g h t [ i ] ) maxLeft[i] = max(maxLeft[i-1], height[i]) maxLeft[i]=max(maxLeft[i−1],height[i])
同理,右侧的最值就是由右边的值与当前值比较获得
m a x R i g h t [ i ] = m a x ( m a x R i g h t [ i + 1 ] , h e i g h t [ i ] ) maxRight[i] = max(maxRight[i+1], height[i]) maxRight[i]=max(maxRight[i+1],height[i])
之后比较获得两者间的最小值后再减去自身高度,就是能容纳的雨水量。
代码:
var trap = function(height) {
const len = height.length
// 一个位置能放多少水是由左侧和右侧最大值中的最小值决定
// 动态规划正反两轮遍历,得到左右两侧最大值进行计算
// 首元素的左侧最大值视为0
const leftMax = new Array(len).fill(0);
// 尾元素的右侧最大值视为0
const rightMax = new Array(len).fill(0);
// 因为循环要用i-1处的值,所以初始化
leftMax[0] = height[0];
rightMax[len-1] = height[len-1];
for(let i=1; i<len; i++){
// 左侧最大值就是由左边的最大值和自己比较决定
leftMax[i] = Math.max(leftMax[i-1], height[i]);
}
for(let i=len-2; i>=0; i--){
// 右侧最大值就是由右边最大值和自己比较决定
rightMax[i] = Math.max(rightMax[i+1], height[i]);
}
// 根据左右两侧最大值进行计算
let result = 0;
for(let i=0; i<len; i++){
let lineHeight = Math.min(leftMax[i], rightMax[i]);
// 左右侧最大值连接的水平线高度减去自身高度,就是当前位置能存的水量
result += Math.max(0, lineHeight - height[i]);
}
return result;
};
复杂度分析
不同于动态规划计算每个位置纵向能容纳的雨水量,单调栈是计算横向能容纳的雨水量。
思路:
https://leetcode-cn.com/problems/trapping-rain-water/solution/trapping-rain-water-by-ikaruga/
如上所示,根据高度划分出不同区域,然后利用宽度乘高度来计算每个区域的面积。
单调栈即遇到大于栈首的元素,就表示划分出一个区域,计算该区域的面积。
高度0也要进栈,方便计算。
每次计算区域时,不光是此时的高度,还要减去基底高度,如上图的浅蓝色区域,要减去上一个区域的高度,所以加入0高度方便计算。
代码:
var trap = function(height) {
// 与动态规划纵向计算每个位置雨水量方法不同的是,单调栈采用横向进行计算每个区域的雨水
const stack = [],
len = height.length;
let res = 0;
for(let i=0; i<len; i++){
while(stack.length && height[i] >= height[stack[stack.length-1]]){
// 栈里存储索引,方便计算
// 构成的区域底部的高度
const bottom_index = stack.pop();
if(stack.length === 0) break
const left_index = stack[stack.length-1];
// 横向的范围
const range = i - left_index - 1;
// 当前高度减去底部高度,乘长度范围得到结果
let temp = (Math.min(height[left_index], height[i]) - height[bottom_index])*range;
res += temp;
}
stack.push(i);
// console.log(stack, res);
}
return res;
};
每次先弹出的这个是基底高度对应的索引。
const bottom_index = stack.pop();
此时的高度是在下面这里,即还在栈顶
// 当前高度减去底部高度,乘长度范围得到结果
let temp = (Math.min(height[left_index], height[i]) - height[bottom_index])*range;
因为积水这个事件是不会在两个相邻的柱子间发生,一定起码有三个柱子才能积水,所以需要一个中间作为基底的值(0也被看作一个基底)。
复杂度分析
使用双指针优化动态规划的思路,达到不使用dp数组的效果。
官方题解:https://leetcode-cn.com/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode-solution-tuvc/
关键在于
// 左指针移动,表示左侧这个值小于右侧值,所以当前位置的左侧最值是小于右侧最值
// 选取两者最小的,即左侧最值进行计算
if(height[left] <= height[right]){
left++;
leftMax = Math.max(leftMax, height[left]);
res += leftMax - height[left];
}
如果左指针所在值小于右指针所在值,表示在左指针这个位置,左侧最值leftMax是一定小于右侧最值rightMax的。
为什么呢?
明白了左侧最值小于右侧最值,那么根据取较小值的规则,就直接使用当前的左侧最值减去当前高度就是能容纳的雨水量了。
同理右侧,也直接用右侧最值计算即可
代码:
var trap = function(height) {
// 双指针 左右两侧双指针,谁的高度低谁移动
const len = height.length;
let left = 0,
right = len - 1,
leftMax = height[left],
rightMax = height[right],
res = 0;
// 分别计算两侧,相遇就表示每个位置都计算完毕
while(left < right){
// 左指针移动,表示左侧这个值小于右侧值,所以当前位置的左侧最值是小于右侧最值
// 选取两者最小的,即左侧最值进行计算
if(height[left] <= height[right]){
left++;
leftMax = Math.max(leftMax, height[left]);
res += leftMax - height[left];
}
else {
// 同理右侧的最值小于左侧最值,所以用右侧最值计算
right--;
rightMax = Math.max(rightMax, height[right]);
res += rightMax - height[right];
}
}
return res;
};
从两侧出发,两侧是不会积水的,所以每次往内部移动时每移动到一个位置,就计算这个位置的积水量,这样当两个指针重合时,就表示所有位置都计算完毕。
复杂度分析