花式接雨水

今天总结一下 LeetCode 上的几道跟接雨水有关的题目。

第 11 题 怎样接最多的雨水

题目链接:11. Container With Most Water

原题如下:

Given n non-negative integers a 1 , a 2 , . . . , a n , a_1, a_2,...,a_n, a1,a2,...,an, where each represents a point at coordinate (i, a i a_i ai). n vertical lines are drawn such that the two endpoints of line i is at (i, a i a_i ai) and (i, 0). Find two lines, which together with x-axis forms a container, such that the container contains the most water.

Note: You may not slant the container and n is at least 2.

题意大概是:

给你 n 个非负整数 a 1 , a 2 , . . . , a n , a_1, a_2,...,a_n, a1,a2,...,an, 每一个数都代表坐标系上的一个点 (i, a i a_i ai)。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, a i a_i ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

注意:你不能倾斜容器,且 n 的值至少为 2。

比如下图中的垂直线可以用数组 [1,8,6,2,5,4,8,3,7] 表示,在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

在这里插入图片描述

思路一:暴力求解

首先想到的就是这道题可以暴力求解,首先从左往右遍历数组中的每一个元素,当遍历到第 i ( 1 ≤ i < n ) i(1\le i< n) i(1i<n) 个元素时,我们依次考察 a i a_i ai a j ( i < j ≤ n ) a_j(i<j\le n) aj(i<jn) 组成的容器能够容纳水的面积,遍历过程中记录下目前位置遇到过的最大的面积,这样,算法结束后,就得到我们想要的结果了。

虽然时间复杂度会很高,但是实现一下也无妨,至少可以用来验证其他算法执行得是否正确。

代码如下:

public int maxArea(int[] height) {
    int maxArea = Integer.MIN_VALUE;
    for (int i = 0; i < height.length - 1; i++) {
        for (int j = i + 1; j < height.length; j++) {
            int area = (j - i) * Math.min(height[j], height[i]);
            if (maxArea < area){
                maxArea = area;
            }
        }
    }
    return maxArea;
}

复杂度分析

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)。因为是两层循环,总的遍历次数为 1 + 2 + ⋯ + n − 1 = n 2 − n 2 1+2+\cdots+n-1=\frac{n^2-n}{2} 1+2++n1=2n2n,取最高阶项,忽略系数,时间复杂度就是 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度:O(1)。算法执行过程中只需额外申请几个变量。

思路二:双指针

我们知道,线与线之间形成的面积总是受到较短的线的高度的限制。两条线相距越远,得到的面积就越大。我们设置两个指针,一个指向数组的开头,另一个指向数组的末尾,再维护一个变量 maxarea,记录目前为止遇到的最大面积,每次计算完两个指针指向的直线构成的面积之后,我们让指向更短的那条直线的指针向前一步,直到两个指针相遇。

LeetCode第11题算法示例

代码如下:

public int maxArea(int[] height){
    int maxArea = Integer.MIN_VALUE;
    for (int i = 0, j = height.length - 1; i < j;) {
        int hi = height[i], hj = height[j];
        int area;
        if (hi >= hj){
            area = (j - i) * hj;
            j--;
        }else {
            area = (j - i) * hi;
            i++;
        }
        if (area > maxArea){
            maxArea = area;
        }
    }
    return maxArea;
}

为什么这种方法是有效的呢?一开始,假设两个水的面积由最外侧的两条直线构成,为了使面积最大,我们需要找到更长的直线。如果我们移动指向较长的那条直线的指针,水的面积不会增大,因为面积取决于较短的那条直线。但是如果移动指向较短的那条直线的指针,尽管两个指针之间的距离缩短了,但是仍然有利于我们找到最大的面积。这样做是因为通过移动短指针获得的相对较长的直线可能会抵消宽度减少所导致的面积减少。

我们也可以用反证法来证明这个算法是有效的。

假设我们得到的结果不是最优解,那么肯定存在一条直线 a_ol 和 a_or 构成最优解。因为只有当两个指针相遇时我们的算法才停止,所以我们一定会遇到 a_ol 和 a_or 中的至少一个,不失一般性,假设我们遇到了 a_ol 没有遇到 a_or,当指针停在 a_ol 上时,不会再往前移动,除非遇到下面两种情况:

  • 另一个指针也指向了 a_ol。在这种情况下,迭代停止,但是另一个指针在到达当前的位置之前一定会遇到 a_or,与我们之前说的不会遇到 a_or 相矛盾。
  • 另一个指针在遇到 a_or 之前遇到了一个比 a_ol 大的值,假设是 a_rr。在这种情况下,我们需要移动 a_ol,但是注意,a_ol 和 a_rr 组成的面积比 a_ol 和 a_or 组成的面积更大,这意味着 a_ol 和 a_or 组成的面积不是最优解。与我们之前的假设矛盾。

所以该算法是有效的。

复杂度分析

  • 时间复杂度:O(n)。
  • 空间复杂度:O(1)。

第 42 题 能够接多少雨水

题目链接:42. Trapping Rain Water

原题如下:

Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it is able to trap after raining.

题意大概是:

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

比如下图是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

在这里插入图片描述

思路一:暴力求解

我们将柱子的个数记为 n,第 i 根柱子的高度记为 h i h_i hi,第 i 根柱子上能接的雨水记为 w i w_i wi,第 i 根柱子左侧最高的柱子的高度记为 m a x l e f t i = m a x { h 1 , h 2 , ⋯   , h i } maxleft_i=max\{h_1,h_2,\cdots,h_i\} maxlefti=max{h1,h2,,hi},第 i 根柱子右侧最高的柱子的高度记为 m a x r i g h t i = m a x { h i , h i + 1 , ⋯   , h n } maxright_i=max\{h_i, h_{i+1},\cdots,h_n\} maxrighti=max{hi,hi+1,,hn},那么 w i = m i n { m a x l e f t i , m a x r i g h t i } − h i w_i=min\{maxleft_i,maxright_i\}-h_i wi=min{maxlefti,maxrighti}hi,将每根柱子能够接的雨水加起来就是我们最后想要的结果了。若结果为 res,那么
r e s = ∑ i = 1 n w i res=\sum^n_{i=1}w_i res=i=1nwi
根据这个思路,可以写出如下代码:

public int trap(int[] height) {
    if (height == null || height.length == 0)
        return 0;
    int res = 0, maxLeft = height[0];
    for (int i = 1; i < height.length - 1; i++) {
        maxLeft = Math.max(maxLeft, height[i]);
        int maxRight = height[i];
        for (int j = i + 1; j < height.length; j++) {
            maxRight = Math.max(maxRight, height[j]);
        }
        res += Math.min(maxLeft, maxRight) - height[i];
    }
    return res;
}

复杂度分析

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)。对于每根柱子 h i h_i hi,都要向左遍历 i 次,找到左侧最高的柱子,向右遍历 n-i 次,找到右侧最高的柱子,加起来就是遍历 n 次,而我们要考察 n 个柱子,所以时间复杂度就是 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度:O(1)。

思路二:动态规划

根据思路一, w i = m i n { m a x l e f t i , m a x r i g h t i } − h i w_i=min\{maxleft_i,maxright_i\}-h_i wi=min{maxlefti,maxrighti}hi,所以每次都要遍历 h i h_i hi 的左右两侧,找到 m a x l e f t i maxleft_i maxlefti m a x r i g h t i maxright_i maxrighti。那我们何不先把 m a x l e f t i maxleft_i maxlefti m a x r i g h t i maxright_i maxrighti 记录下来呢,当需要用到的时候直接取就可以了。也就是说,在计算能够接多少雨水之前,我们先生成两个数组,maxleft 和 maxright,其中 m a x l e f t i maxleft_i maxlefti 是第 i 根柱子左侧最高的柱子的高度, m a x r i g h t i maxright_i maxrighti 是第 i 根柱子右侧最高的柱子的高度,最后再遍历一遍柱子的数组,算出 ∑ i = 1 n ( m i n { m a x l e f t i , m a x r i g h t i } − h i ) \sum^n_{i=1}(min\{maxleft_i,maxright_i\}-h_i) i=1n(min{maxlefti,maxrighti}hi) 就可以了。

代码如下:

public int trap_dp(int[] height) {
    if (height == null || height.length == 0)
        return 0;
    int res = 0, len = height.length;
    int[] left_max = new int[len];
    int[] right_max = new int[len];
    left_max[0] = height[0];
    for (int i = 1; i < len; i++) {
        left_max[i] = Math.max(left_max[i - 1], height[i]);
    }
    right_max[len - 1] = height[len - 1];
    for (int i = len - 2; i >= 0; i--) {
        right_max[i] = Math.max(right_max[i + 1], height[i]);
    }
    for (int i = 0; i < len; i++) {
        res += Math.min(left_max[i], right_max[i]) - height[i];
    }
    return res;
}

复杂度分析

  • 时间复杂度:O(n)。总共需要遍历 3 次柱子的数组。
  • 空间复杂度:O(n)。需要额外的 2n 的内存来存储 maxleft 和 maxright。

思路三:栈

我们知道,一根柱子,只有它两侧都有比它高的柱子的时候,才能够存下水。首先我们让第一根柱子的索引入栈,即让 1 入栈,接下来从第 2 根柱子开始,我们将当前遍历到的柱子高度记为 h i h_i hi,栈顶记为 top,因为栈中存的是索引,所以栈顶柱子的高度就是 h t o p h_{top} htop,根据以下几种情况,做相应的操作:

  • 如果 h i < h t o p h_i<h_{top} hi<htop,就让 i 入栈。
  • 如果 h i = h t o p h_i=h_{top} hi=htop,就让 top 出栈,i 入栈。循环此操作,直至条件不满足。
  • 如果 h i > h t o p h_i>h_{top} hi>htop 并且栈中元素个数小于 2,就让 top 出栈,i 入栈。循环此操作,直至条件不满足。
  • 如果 h i > h t o p h_i>h_{top} hi>htop 并且栈中元素个数大于等于 2(要想存下雨水,至少需要三根柱子),就让 top 出栈,记为 j,此时新的栈顶柱子高度 h t o p > h j h_{top}>h_j htop>hj,第 i 根柱子和第 top 根柱子之间的距离是 i-top - 1,我们记为 distance,那么这两根柱子之间能够存的雨水就是 ( m i n { h t o p , h i } − h j ) × d i s t a n c e (min\{h_{top},h_i\}-h_j)\times distance (min{htop,hi}hj)×distance。循环此操作,直至条件不满足。

有了上述思路,就可以写出代码了。

public int trap_stack2(int[] height){
    if (height == null || height.length == 0)
        return 0;
    int res = 0;
    Stack<Integer> stack = new Stack<>();
    stack.push(0);
    for (int i = 1; i < height.length; i++) {
        while (stack.size() >= 2 && height[i] > height[stack.peek()]) {
            int top = stack.pop();
            int leftBoundIdx = stack.peek();
            int leftBound = height[leftBoundIdx], distance = i - leftBoundIdx - 1;
            res += (Math.min(leftBound, height[i]) - height[top]) * distance;
        }
        while (!stack.isEmpty() && height[i] > height[stack.peek()]){
            stack.pop();
        }
        stack.push(i);
    }
    return res;
}

复杂度分析

  • 时间复杂度:O(n)。最坏情况下,前 n-1 个柱子的高度依次递减,第 n 个柱子的高度是所有柱子中最高的,在这种情况下,前 n-1 个柱子依次进栈后,又会逐个出栈,这样的话,时间复杂度就是 O(n)。
  • 空间复杂度:O(n)。

思路四:双指针(大杀器)

我们用两个指针 i 和 j,刚开始他们分别指向第一个柱子和最后一个柱子,两个指针相向而行,在前进的过程中,维护两个变量: maxleft 和 maxright,分别表示 h i h_i hi 左边最高的柱子的高度和 h j h_j hj 右边最高的柱子的高度。

当 i<j 时,循环以下操作:

  1. 如果 h i > m a x l e f t h_i>maxleft hi>maxleft,则将 maxleft 的值更新为 h i h_i hi

  2. 如果 h j > m a x r i g h t h_j>maxright hj>maxright,则将 maxright 的值更新为 h j h_j hj

  3. 如果 maxleft < maxright,计算 h i h_i hi 能存的雨水 w i = m a x l e f t − h i w_i=maxleft-h_i wi=maxlefthi,i 前进一步。

    否则计算 h j h_j hj 能存的雨水 w j = m a x r i g h t − h j w_j=maxright-h_j wj=maxrighthj,j 前进一步。

将每次计算得到的雨水加起来就是最终的结果了。以下是代码实现:

public int trap_TwoPoints(int[] height){
    if (height == null || height.length == 0)
        return 0;
    int i = 0, j = height.length - 1;
    int maxLeft = 0, maxRight = 0, res = 0;
    while (i < j){
        if (height[i] > maxLeft){
            maxLeft = height[i];
        }
        if (height[j] > maxRight){
            maxRight = height[j];
        }
        if (maxLeft < maxRight){
            res += maxLeft - height[i];
            i++;
        }else {
            res += maxRight - height[j];
            j--;
        }
    }
    return res;
}

简单解释一下这个思路为什么有效。上述代码中的 maxleft 相当于我们之前说的 m a x l e f t i maxleft_i maxlefti ,maxright 相当于 m a x r i g h t j maxright_j maxrightj 我们知道 w i w_i wi 取决于 m i n { m a x l e f t i , m a x r i g h t i } min\{maxleft_i,maxright_i\} min{maxlefti,maxrighti},由于 i<j,所以 m a x r i g h t j ≤ m a x r i g h t i maxright_j\le maxright_i maxrightjmaxrighti 一定成立。如果 m a x l e f t i < m a x r i g h t j maxleft_i<maxright_j maxlefti<maxrightj,那么 m a x l e f t i maxleft_i maxlefti 一定小于 m a x r i g h t i maxright_i maxrighti,所以 w i = m a x l e f t i − h i w_i=maxleft_i-h_i wi=maxleftihi,同理,如果 m a x l e f t i ≥ m a x r i g h t j maxleft_i\ge maxright_j maxleftimaxrightj,那么 w j = m a x r i g h t j − h j w_j=maxright_j-h_j wj=maxrightjhj

复杂度分析

  • 时间复杂度:O(n)。
  • 空间复杂度:O(1)。

第 407 题 第 42 题升级版——三维地图接雨水

题目链接:407. Trapping Rain Water II

原题如下:

Given an m x n matrix of positive integers representing the height of each unit cell in a 2D elevation map, compute the volume of water it is able to trap after raining.

Note: Both m and n are less than 110. The height of each unit cell is greater than 0 and is less than 20,000.

题意大概是:

给定一个 m × n m\times n m×n 的矩阵,其中的值均为正整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。

注意:m 和 n 都是小于 110 的整数。每一个单位的高度都大于 0 且小于 20000。

如下图所示,这是下雨前的状态,其高度图可以用这个数组表示:

{
    {1,4,3,1,3,2},
    {3,2,1,3,2,4},
    {2,3,3,2,3,1}
}

在这里插入图片描述

下雨后,雨水将会被存储在这些方块中。总的接雨水量是4。
在这里插入图片描述

解题思路

首先我们把地图最外层的那一圈柱子逐个放入优先级队列(小顶堆)queue,作为水池的「外壁」,然后从 queue 中取最矮的那根柱子 min,同时记录已经出队的柱子中最高的高度 maxbound,maxbound 就是当前水池能够存的最大水位。我们分别考察 min 的上下左右相邻的柱子 min_neibor 是否比 maxbound 矮,如果是话,说明可以存水,那我们的存水量就加上「min_neibor - maxbound」。

代码如下:

class Solution {
    //地图上的柱子
    private static class Bar implements Comparable<Bar>{
        int i; //柱子所在的行
        int j; //柱子所在的列
        int height; //柱子的高度
        Bar(int i, int j, int height){
            this.i = i;
            this.j = j;
            this.height = height;
        }

        @Override
        public int compareTo(Bar o) {
            return this.height - o.height;
        }
    }
    public int trapRainWater(int[][] heightMap) {
        if (heightMap == null || heightMap.length < 3 || heightMap[0].length < 3){
            return 0;
        }
        int m = heightMap.length, n = heightMap[0].length;
        boolean[][] visited = new boolean[m][n];
        Queue<Bar> pq = new PriorityQueue<>();
        for (int i = 0; i < m; i++) {
            Bar bar1 = new Bar(i, 0, heightMap[i][0]);
            pq.add(bar1);
            visited[i][0] = true;
            Bar bar2 = new Bar(i, n - 1, heightMap[i][n - 1]);
            pq.add(bar2);
            visited[i][n - 1] = true;
        }
        for (int i = 1; i < n - 1; i++) {
            Bar bar1 = new Bar(0, i, heightMap[0][i]);
            pq.add(bar1);
            visited[0][i] = true;
            Bar bar2 = new Bar(m - 1, i, heightMap[m - 1][i]);
            pq.add(bar2);
            visited[m - 1][i] = true;
        }
        int res = 0;
        int maxBound = Integer.MIN_VALUE; //相当于二维空间里面的maxLeft,即能够拦截住的最大水位
        while (!pq.isEmpty()){
            Bar bar = pq.poll();
            maxBound = Math.max(maxBound, bar.height);
            Bar temp;
            if (bar.i - 1 >= 0 && !visited[bar.i - 1][bar.j]){
                temp = new Bar(bar.i - 1, bar.j, heightMap[bar.i - 1][bar.j]);
                pq.add(temp);
                visited[bar.i - 1][bar.j] = true;
                if (maxBound > temp.height){
                    res += maxBound - temp.height;
                }
            }
            if (bar.i + 1 < m && !visited[bar.i + 1][bar.j]){
                temp = new Bar(bar.i + 1, bar.j, heightMap[bar.i + 1][bar.j]);
                pq.add(temp);
                visited[bar.i + 1][bar.j] = true;
                if (maxBound > temp.height){
                    res += maxBound - temp.height;
                }
            }
            if (bar.j - 1 >= 0 && !visited[bar.i][bar.j - 1]){
                temp = new Bar(bar.i, bar.j - 1, heightMap[bar.i][bar.j - 1]);
                pq.add(temp);
                visited[bar.i][bar.j - 1] = true;
                if (maxBound > temp.height){
                    res += maxBound - temp.height;
                }
            }
            if (bar.j + 1 < n && !visited[bar.i][bar.j + 1]){
                temp = new Bar(bar.i, bar.j + 1, heightMap[bar.i][bar.j + 1]);
                pq.add(temp);
                visited[bar.i][bar.j + 1] = true;
                if (maxBound > temp.height){
                    res += maxBound - temp.height;
                }
            }
        }
        return res;
    }
}

复杂度分析

  • 时间复杂度: O ( m n log ⁡ ( m + n ) ) O(mn\log(m+n)) O(mnlog(m+n))。因为要地图的大小是 m × n m\times n m×n,要遍历一遍的时间复杂度是 O ( m n ) O(mn) O(mn),每次迭代时都要从优先级队列中取最小值,由于优先级队列中大概有 2m+2n 个元素,所以这个操作的时间复杂度是 O(m+n),所以总的时间复杂度是 O ( m n log ⁡ ( m + n ) ) O(mn\log(m+n)) O(mnlog(m+n))
  • 空间复杂度:O(m+n)。因为需要存储优先级队列。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值