leetcode接雨水

leetcode接雨水

让我们来切土豆

题目
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水
leetcode接雨水_第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/

解法

1. 动态规划

动态规划的思路就是纵方向来看,计算每个位置能容纳多少雨水。
每个位置能容纳雨水的量是由该位置左边最高点和右边最高点决定。

leetcode接雨水_第2张图片
如图,位置 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[i1],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;

};

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n) n n n为数组长度,需要遍历三轮进行计算。
  • 空间复杂度: O ( n ) O(n) O(n), 两个数组存储最值。

2. 单调栈

不同于动态规划计算每个位置纵向能容纳的雨水量,单调栈是计算横向能容纳的雨水量。

思路:
https://leetcode-cn.com/problems/trapping-rain-water/solution/trapping-rain-water-by-ikaruga/

leetcode接雨水_第3张图片

如上所示,根据高度划分出不同区域,然后利用宽度乘高度来计算每个区域的面积。

单调栈即遇到大于栈首的元素,就表示划分出一个区域,计算该区域的面积。
高度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也被看作一个基底)。


复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n) n n n为数组长度,需要遍历进行计算,每个元素最多入栈出栈各一次,所以不是 n 平方。
  • 空间复杂度: O ( n ) O(n) O(n), 单调栈存储高度值。

3. 双指针优化

使用双指针优化动态规划的思路,达到不使用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的。

为什么呢?

  • 如果左指针处指的就是leftMax,而且小于右指针值,则确实小于rightMax了。
  • 如果左指针指的不是leftMax,则根据移动规则,哪一个指针值小哪一个就移动,说明该左指针是从左侧最值移动过来的。即右侧有值大于左侧最值leftMax,迫使左指针发生移动,所以还是有 l e f t M a x < r i g h t M a x leftMaxleftMax<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;
};

从两侧出发,两侧是不会积水的,所以每次往内部移动时每移动到一个位置,就计算这个位置的积水量,这样当两个指针重合时,就表示所有位置都计算完毕。

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n) n n n为数组长度,需要遍历进行计算。
  • 空间复杂度: O ( 1 ) O(1) O(1), 只需要常数空间。

你可能感兴趣的:(leetcode刷题,动态规划,数组,动态规划,单调栈,双指针)