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

题解分析

解法一:暴力法

  1. 暴力法比较简单,但是需要一些思考(结合图中柱子的特点)。
  2. 分别找出当前柱子左边和右边的最大值,然后取这两个值的最小值作为边界(利用短板原理)。
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) 的额外空间。

解法二:方法一优化,动态编程

  1. 可以使用空间换时间,提前使用两个数字来存储当前柱子左右的最大高度
  2. 在循环时即可直接索引数组中的值
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;
    }
}

解法三:双指针法

  1. 和方法 2 相比,我们不从左和从右分开计算,我们想办法一次完成遍历。
  2. 从动态编程方法的示意图中我们注意到,只要 \(right_{max}[i]>left_{max}[i]\) (元素 0 到元素 6),积水高度将由 left_max 决定,类似地 \(left_{max}[i]>right_{max}[i]\)(元素 8 到元素 11)。
  3. 所以我们可以认为如果一端有更高的条形块(例如右端),积水的高度依赖于当前方向的高度(从左到右)。当我们发现另一侧(右侧)的条形块高度不是最高的,我们则开始从相反的方向遍历(从右到左)。
  4. 我们必须在遍历时维护 \(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;
    }
}

方法四:单调栈

  1. 除了使用双指针法,本题也可以使用单调栈来解决,因为当依次从左到右考虑时,可以根据一定的规律不断计算出一部分的答案。
  2. 那这种特殊的规律是什么呢?换句话说,当遇到什么情况可以计算出一部分的答案呢?其实仔细看样例,我们可以发现,每次遇到当前高度比前一个高度高时就可以计算出低洼处的面积。
  3. 所以,为了可以每次不断计算低洼处的面积,我们维护一个单调递减栈,每次都保存栈是单调递减的(只有是单调递减的,我们才能当遇到一个更高的柱子时不断计算前面的低洼处)。
  4. 需要注意的是,我们的单调栈中存放的不是高度,而是索引,这点容易被忽略。之所以存储索引,是因为我们需要计算面积,通过索引可以计算宽度。
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;
    }
}

参考

  1. 如何高效解决接雨水问题
posted @ 2021-03-08 21:12  Garrett_Wale  阅读(207)  评论(0编辑  收藏  举报