042J. Trapping Rain Water

https://leetcode.com/problems/trapping-rain-water/solution/

这个题如此重要,频率顺序第三,hard 第一。非常值得一鸭三吃 参考了这个同学的一些方法,Credit 是要给他的。 https://leetcode.wang/leetCode-42-Trapping-Rain-Water.html

暴力


一般我们都自动忽略暴力法,对于这个题
理解暴力法在这里反而非常重要。
对于每一个位置 i ,分别找到它左侧和右侧最高的高度,如果两个
高度都比这个位置 i 高,说明这里可以储存下水,反之不行。
因此可以轻松的计算下这个位置 i 储存的水量。
全部加起来就可以了。

这个方法是标准的O(N^2), 空间O(1)
class Solution {
    public int trap(int[] height) {
        int ans = 0;

        int n  = height.length;
        for(int i = 1; i < n - 1; i++){
          //  找左侧最高
            int maxL = 0;
            for(int j = i - 1; j > -1; j--){
                if(height[j] > maxL){
                    maxL = height[j];
                }
            }
            // 找右侧最高
            int maxR = 0;
            for(int j = i+1; j < n; j++){
                if(height[j] > maxR){
                    maxR = height[j];
                }
            }
            //判断是否储存水
            if(height[i] < Math.min(maxL,maxR)){
                ans += Math.min(maxL,maxR) - height[i];
            }
        }
        return ans;
    }
}

Method 2 : Dynamic Programming || 使用数组


对于上面的暴力算法
我们只需要做一点点改进,就可以使我们的效率大大提高。
我们注意到,反复的扫描得到maxR 和 maxL 是效率很低的,
如果我们用两个数组分别保存所有对应位置的maxR 和 maxL,
我们只需要两次扫描,加上一次最终计算的扫描,就可以得到
答案了。

现在我们是 O(N), O(N)
上面暴力法我的提交是68毫秒。而改进之后已经是1毫秒了。
class Solution {
    public int trap(int[] height) {
        if(height.length == 0) return 0;
        int ans = 0;
        int n = height.length;
        int[] maxL = new int[n];
        int[] maxR = new int[n];
        //maxL从左到右扫描,maxR 从右到左扫描,第一个位置需要初始化。
        maxL[0] = 0;
        maxR[n - 1] = 0;
        for(int i = 1; i < n; i++){
            maxL[i] = Math.max(height[i - 1],maxL[i - 1]);
        }
        for(int i = n - 2; i > - 1; i--){
            maxR[i] = Math.max(height[i + 1], maxR[i + 1]);
        }
        for(int i  = 1; i < n - 1; i++){
            int shortBoard = Math.min(maxL[i], maxR[i]);
            ans +=  shortBoard > height[i] ? shortBoard- height[i] : 0;
        }
        return ans;
    }
}

Method 3:


上面这个方法我们发现,每一对maxL 和 maxR 我们用了就扔了。对空间比较浪费。
我们先想一个可以的优化,首先上面的三个for循环中,前两个可以减少至少一个。
具体减少哪一个取决于我们从左到右还是从右到左来计算水。
如果是从左到右,由于我们可以一直维护maxL, 所以这个数组是不需要的。具体看代码
class Solution {
    public int trap(int[] height) {
        if(height.length == 0) return 0;
        int ans = 0;
        int n = height.length;

        int[] maxR = new int[n];
        maxR[n-1] = 0;
        for(int i = n - 2; i > -1; i--){
            maxR[i] = Math.max(maxR[i+1],height[i+1]);
        }

        int maxL = 0;
        for(int i = 1; i < n ; i++){
            // 核心就在下面这行代码。一直帮我们用一个指针记录了最大的右点。
            maxL = Math.max(maxL, height[i-1]);
            int temp = Math.min(maxL,maxR[i]);
            if(temp > height[i]){
                ans += temp - height[i];
            }
        }
        return ans;
    }
}

有了上面的灵感,我们就得到了最好的方法。
上面的方法既可以从左到右递增指标i,也可以写成从右到左递减指标i。
每一种方法都只能减少一个for循环和一个数组。如果能结合这两个套路,岂不美哉?

所以最佳的方法是这样的,与其用顺序变化的指标 i, 不如灵活一点,
我们维护一对双指针,适合从左到右的时候就从左到右,适合从右到左的时候就从右到左。
如何判定这个条件呢?我们设 left 和 right 分别从 1 和 n - 2处 待命。我们判定
maxL 和 maxR 主要看的是 left - 1 和 right + 1。
      maxL = Math.max(height[left - 1],maxL)
      maxR = Math.max(height[right + 1], maxR);
如果 maxL 比 maxR 小,那么我们暂时没必要去动maxR,我们就一直动maxL这边的left,直到
maxL比maxR大了,我们再去移动right。这样当交汇的时候,就得到了所有的答案。
class Solution {
    public int trap(int[] height) {
        // if(height.length < 3) return 0;
        int ans = 0;
        int n = height.length;
        int maxL = 0;
        int maxR = 0;
        int left = 1;
        int right = n - 2;
        //判断语句也可以用下面这句,作用一样,总之就是让他们
        //运行 n - 2 次之后停止循环,因为我们只需要检查n - 2个位置。
        //for(int i = 1; i < n - 1; i++)
        while(left <= right) {
            maxL = Math.max(height[left - 1],maxL);
            maxR = Math.max(height[right + 1], maxR);
            if(maxL < maxR){
              //下面是一种省力的方法 省去了一个 if判断
                ans += Math.max(0,maxL - height[left]);
                left++;
            }else{
                ans += Math.max(0,maxR - height[right]);
                right--;
            }
        }
        return ans;
    }
}

Method 4 : Stack


用栈,我们存什么?存的是数组位置i。
我们要找什么?我们要找数组先降后升的特征,才能储存水。
因此数组不上升的话我们就入栈。如果下一个准备入栈的数增大了,
我们就需要准备处理出栈了。
我们的思路还是 maxL 和 maxR,
我们把当前的pos当作 maxR, 出栈一个当被考虑当位置,
然后看出栈之后当栈顶,直接把它当作maxL,这样就知道该处的深度了。
然后把宽度计算一下,就知道水的“面积”了。
class Solution {
    public int trap(int[] height) {
        int ans = 0;
        Stack<Integer> stack = new Stack<>();
        int pos = 0;
        while(pos < height.length){
          // 下面的while中 <= 也不影响结果,为什么?因为我们计算水的面积是一横条一横条计算的。
          // 所以不会影响结果 <= 对应的是 如果数组下降 我们就入栈,不下降就出栈。比如
          // 2,1,1,1,1,1,在栈中只会有 2,1,而忽略掉重复的对应的位置,这里的1是最后一个1。

            while(!stack.isEmpty() && height[stack.peek()] < height[pos] ){
                int maxR = height[pos];
                int h = height[stack.pop()];
                //栈空了就直接结束循环
                if(stack.isEmpty()) break;
                int maxL = height[stack.peek()];
                int distance = pos - stack.peek() - 1;
                int hight = Math.max(0,Math.min(maxR,maxL) - h);
                ans += distance * hight;
            }
            //如果不需要出栈 那么就入栈
            stack.push(pos);
            pos++;
        }
        return ans;
    }
}

=====

随便记录一个之前写的答案

class Solution {
    public int trap(int[] height) {
        if(height.length < 3) {
            return 0;
        }
        int maxL = height[0];
        int maxR = height[height.length - 1];
        int left = 1;
        int right = height.length - 2;
        int volume = 0;
        while( left <= right ) {
            if(maxL < maxR ) {
                volume += Math.max(0, maxL - height[left]);
                maxL = Math.max(maxL, height[left]);
                left++;
            }else{
                volume += Math.max(0, maxR - height[right]);
                maxR = Math.max(maxR, height[right]);
                right--;
            }
        }
        return volume;
    }
}

Last updated

Was this helpful?