从时空复杂度出发,逐步优化接雨水问题

接雨水问题


题目描述

给定 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 = = h e i g h t . l e n g t h n == height.length n==height.length
  • 0 < = n < = 3 ∗ 104 0 <= n <= 3 * 104 0<=n<=3104
  • 0 < = h e i g h t [ i ] < = 105 0 <= height[i] <= 105 0<=height[i]<=105

如图所示,对于某一个具体的位置 h e i g h t [ i ] height[i] height[i]来说,给定的柱子宽度都是1,因此,该位置可能接的雨水量就只和高度相关。根据木桶盛水的短板原则, h e i g h t [ i ] height[i] height[i]位置能接多少水,还取决于它两边的情况。即左边的最大高度 m a x _ l e f t max\_left max_left和右边的最大高度 m a x _ r i g h t max\_right max_right,并且最多接水量只和两者中最小的高度 m i n ( m a x _ l e f t , m a x _ r i g h t ) min(max\_left, max\_right) min(max_left,max_right)相关。

  • 如果 m i n ( m a x _ l e f t , m a x _ r i g h t ) > h e i g h t [ i ] min(max\_left, max\_right) > height[i] min(max_left,max_right)>height[i],那么 h e i g h t [ i ] height[i] height[i]位置接水量就是两者的差值
  • 如果 min ⁡ ( m a x _ l e f t , m a x _ r i g h t ) < h e i g h t [ i ] \min(max\_left, max\_right) < height[i] min(max_left,max_right)<height[i],那么 h e i g h t [ i ] height[i] height[i]位置无法接水

如何理解呢?以题目中给的🌰为例:

  • 如果当前位置为 h e i g h t [ 1 ] height[1] height[1],那么情况如下所示:


    在这里插入图片描述

    左边最高为0,右边最高为3,当前位置为1。根据上述的原则,该位置无法接水

  • 如果当前位置为 h e i g h t [ 3 ] height[3] height[3],情况如下所示:


    在这里插入图片描述

    左边最高为1,右边最高为3,当前位置为1,那么该位置也无法接水

  • 如果当前位置为 h e i g h t [ 7 ] height[7] height[7],情况如下所示:


    在这里插入图片描述

    左边最高为2,右边最高为3,当前位置为1。满足上述能接水的原则,因此,该位置接水量为
    min ⁡ ( m a x _ l e f t , m a x _ r i g h t ) − h e i g h t [ 7 ] = min ⁡ ( 2 , 3 ) − 1 = 1 \min(max\_left,max\_right) - height[7]=\min(2,3)-1 = 1 min(max_left,max_right)height[7]=min(2,3)1=1

所以,问题的关键就在于如何找到某一位置 h e i g h t [ i ] height[i] height[i]的左最高 m a x _ l e f t max\_left max_left和右最高 m a x _ r i g h t max\_right max_right

首先,我们来看最直观的方法,即到达一个具体的位置先找 m a x _ l e f t max\_left max_left m a x _ r i g h t max\_right max_right。假设当前位置为 h e i g h t [ i ] height[i] height[i],那么 m a x _ l e f t max\_left max_left只能在它的左半部分找且不包含自身,即:

int max_left = 0;
for(int j = i - 1; j >= 0; j--){
    if(height[j] > max_left){
        max_left = height[j];
    }
}

对应的 m a x _ r i g h t max\_right max_right只能在它的右半部分找且不含自身,即:

int max_right = 0;
for(int j = i + 1; j < height.length; j++){
    if(height[j] > max_right){
        max_right = height[j];
    }
}

当找到了 m a x _ l e f t max\_left max_left m a x _ r i g h t max\_right max_right,按照前面的分析,自然就可以得到该位置的接水量。由于两端不可能接雨水,因此,讨论的范围为 [ 1 , l e n ( h e i g h t ) − 1 ) [1, len(height) - 1) [1,len(height)1)。下面逐步的通过图示的方式理解下上述的流程。

  • h i e g h t [ 1 ] hieght[1] hieght[1]:当前位置为1, m a x _ l e f t = 0 max\_left=0 max_left=0 m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为0


    在这里插入图片描述

  • h i e g h t [ 2 ] hieght[2] hieght[2]:当前位置为0, m a x _ l e f t = 1 max\_left=1 max_left=1 m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为1


    在这里插入图片描述

  • h i e g h t [ 3 ] hieght[3] hieght[3]:当前位置为12, m a x _ l e f t = 1 max\_left=1 max_left=1 m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为0


    在这里插入图片描述

  • h i e g h t [ 4 ] hieght[4] hieght[4]:当前位置为2, m a x _ l e f t = 1 max\_left=1 max_left=1 m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为0


    在这里插入图片描述

  • h i e g h t [ 5 ] hieght[5] hieght[5]:当前位置为1, m a x _ l e f t = 2 max\_left=2 max_left=2 m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为1


    在这里插入图片描述

  • h i e g h t [ 6 ] hieght[6] hieght[6]:当前位置为0, m a x _ l e f t = 2 max\_left=2 max_left=2 m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为2


    在这里插入图片描述

  • h i e g h t [ 7 ] hieght[7] hieght[7]:当前位置为1, m a x _ l e f t = 2 max\_left=2 max_left=2 m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为1


    在这里插入图片描述

  • h i e g h t [ 10 ] hieght[10] hieght[10]:当前位置为1, m a x _ l e f t = 3 max\_left=3 max_left=3 m a x _ r i g h t = 2 max\_right=2 max_right=2,接水量为0


    在这里插入图片描述

  • h i e g h t [ 11 ] hieght[11] hieght[11]:当前位置为2, m a x _ l e f t = 3 max\_left=3 max_left=3 m a x _ r i g h t = 1 max\_right=1 max_right=1,接水量为0


    在这里插入图片描述

通过上述的逐步分析,最终的接水量为6。详细的解题代码如下所示:

/**
 * @Author dyliang
 * @Date 2020/11/10 22:39
 * @Version 1.0
 */
public class _42 {
    public static void main(String[] args) {
        int[] height = {0,1,0,2,1,0,1,3,2,1,2,1};
        System.out.println(trap4(height));
    }

    public static int trap2(int[] height){
        int sum = 0;
        for (int i = 1; i < height.length - 1; i++) {
            int max_left = 0;
            for(int j = i - 1; j >= 0; j--){
                if(height[j] > max_left){
                    max_left = height[j];
                }
            }

            int max_right = 0;
            for(int j = i + 1; j < height.length; j++){
                if(height[j] > max_right){
                    max_right = height[j];
                }
            }

            int min_one = Math.min(max_left,max_right);
            sum += Math.max(0, min_one - height[i]);
        }
        return sum;
    }
}

仔细分析,这种解法需要每个位置都遍历一次给定的数组找 m a x _ l e f t max\_left max_left m a x _ r i g h t max\_right max_right,时间复杂度为 O ( N 2 ) O(N^2) O(N2)。由于过程中没有使用额外的存储空间,空间复杂度为 O ( 1 ) O(1) O(1)

如果详细的走过每一个位置可以发现, m a x _ l e f t max\_left max_left m a x _ r i g h t max\_right max_right在很多位置上并没有发生改变,如下图黄框所示的位置,每一个框内所有位置的的 m a x _ l e f t max\_left max_left m a x _ r i g h t max\_right max_right都是相同的。如果我们提前将每个位置的 m a x _ l e f t max\_left max_left m a x _ r i g h t max\_right max_right找到并存储起来,那么求接水量的时间复杂度就会下降到 O ( N ) O(N) O(N)。虽然,此时空间复杂度变成了 O ( N ) O(N) O(N),但是空间换时间是完全可接受的。


在这里插入图片描述

那么如何找每个位置的 m a x _ l e f t max\_left max_left m a x _ r i g h t max\_right max_right?假设从左往右 m a x _ l e f t max\_left max_left,当前遍历到了 h e i g h t [ i ] height[i] height[i],那么它对应的 m a x _ l e f t [ i ] max\_left[i] max_left[i]取决于 h e i g h t [ i − 1 ] height[i-1] height[i1] h e i g h t [ i − 1 ] height[i-1] height[i1]位置对应的 m a x _ l e f t [ i − 1 ] max\_left[i-1] max_left[i1],而且 m a x _ l e f t [ i ] max\_left[i] max_left[i]的值是两者的最大值。

m a x _ r i g h t max\_right max_right数组的值取决于每个位置右半部分的情况,因此需要从右往左遍历,对应的原则是 m a x _ r i g h t [ i ] = max ⁡ ( m a x _ r i g h t [ i + 1 ] , h e i g h t [ i + 1 ] ) max\_right[i] = \max(max\_right[i + 1], height[i + 1]) max_right[i]=max(max_right[i+1],height[i+1])

详细的解题代码如下:

/**
 * @Author dyliang
 * @Date 2020/11/10 22:39
 * @Version 1.0
 */
public class _42 {
    public static void main(String[] args) {
        int[] height = {0,1,0,2,1,0,1,3,2,1,2,1};
        System.out.println(trap4(height));
    }

    public static int trap3(int[] height){
        int sum = 0;
        int h = height.length;
        int[] max_left = new int[h];
        int[] max_right = new int[h];

        for (int i = 1; i < h - 1; i++) {
            max_left[i] = Math.max(max_left[i - 1], height[i - 1]);
        }

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

        for (int i = 1; i < h - 1; i++) {
            int min_one = Math.min(max_left[i], max_right[i]);
            sum += Math.max(0, min_one - height[i]);
        }
        return sum;
    }
}

上面基于备忘录的方法的时间复杂度和空间复杂度都是 O ( N ) O(N) O(N),由于总是至少要遍历一遍数组,因此,时间复杂度最低也只能是 O ( N ) O(N) O(N),那么能否不用额外的存储空间完成呢

仔细分析上面的解法可以发现,对于 h e i g h t [ i ] height[i] height[i]来说,它只关心它对应的 m a x _ l e f t max\_left max_left,而不关心曾经出现过的 m a x _ l e f t max\_left max_left。同样对于 m a x _ r i g h t max\_right max_right来说,也只关心它对应的 m a x _ r i g h t max\_right max_right,而不关心曾经的 m a x _ r i g h t max\_right max_right。因此,我们可以使用两个变量来记录目前最大的 m a x _ l e f t max\_left max_left m a x _ r i g h t max\_right max_right。而两端位置的遍历,可以使用指针 l e f t left left r i g h t right right进行记录。

而且对于 l e f t left left来说,它对应的 m a x _ l e f t max\_left max_left是不会在变化的,而此时的 m a x _ r i g h t max\_right max_right却不一定。因为, l e f t left left m a x _ r i g h t max\_right max_right对应索引位置之间可能存在更大的值。同样,对于 r i g h t right right来说, m a x _ r i g h t max\_right max_right是不会变化的,但是 m a x _ l e f t max\_left max_left却可能变化。但是,只要 m a x _ l e f t < m a x _ r i g h t max\_left < max\_right max_left<max_right成立,即时出现更大的 m a x _ r i g h t max\_right max_right,根据接水的原则可知,它不会影响当前的结果。同理,只要 m a x _ r i g h t > m a x _ l e f t max\_right > max\_left max_right>max_left成立,即时出现更大的 m a x _ l e f t max\_left max_left,同样不会影响当前的结果。

详细的解题代码如下:

/**
 * @Author dyliang
 * @Date 2020/11/10 22:39
 * @Version 1.0
 */
public class _42 {
    public static void main(String[] args) {
        int[] height = {0,1,0,2,1,0,1,3,2,1,2,1};
        System.out.println(trap4(height));
    }

     public static int trap(int[] height) {
        int sum = 0;
        int left = 0, right = height.length - 1;
        int left_max = 0, right_max = 0;
        while(left <= right){
            // 处理left所指的位置
            if(left_max < right_max){
                // 记录接水量
                sum += Math.max(0, left_max - height[left]);
                // 更新可能的max_left
                left_max = Math.max(left_max, height[left]);
                left++;
             // 处理right所指的位置
            } else {
                // 记录接水量
                sum += Math.max(0, right_max - height[right]);
                // 更新可能的max_right
                right_max = Math.max(right_max, height[right]);
                right--;
            }
        }
        return sum;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值