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

本文详细解析了接雨水问题的算法思路,介绍了如何计算给定柱子高度图中能接多少雨水。通过直观方法、备忘录优化及双指针技巧逐步优化解决方案,降低时间复杂度。

接雨水问题


题目描述

给定 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.lengthn == height.lengthn==height.length
  • 0<=n<=3∗1040 <= n <= 3 * 1040<=n<=3104
  • 0<=height[i]<=1050 <= height[i] <= 1050<=height[i]<=105

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

  • 如果min(max_left,max_right)>height[i]min(max\_left, max\_right) > height[i]min(max_left,max_right)>height[i],那么height[i]height[i]height[i]位置接水量就是两者的差值
  • 如果min⁡(max_left,max_right)<height[i]\min(max\_left, max\_right) < height[i]min(max_left,max_right)<height[i],那么height[i]height[i]height[i]位置无法接水

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

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


    在这里插入图片描述

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

  • 如果当前位置为height[3]height[3]height[3],情况如下所示:


    在这里插入图片描述

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

  • 如果当前位置为height[7]height[7]height[7],情况如下所示:


    在这里插入图片描述

    左边最高为2,右边最高为3,当前位置为1。满足上述能接水的原则,因此,该位置接水量为
    min⁡(max_left,max_right)−height[7]=min⁡(2,3)−1=1\min(max\_left,max\_right) - height[7]=\min(2,3)-1 = 1min(max_left,max_right)height[7]=min(2,3)1=1

所以,问题的关键就在于如何找到某一位置height[i]height[i]height[i]的左最高max_leftmax\_leftmax_left和右最高max_rightmax\_rightmax_right

首先,我们来看最直观的方法,即到达一个具体的位置先找max_leftmax\_leftmax_leftmax_rightmax\_rightmax_right。假设当前位置为height[i]height[i]height[i],那么max_leftmax\_leftmax_left只能在它的左半部分找且不包含自身,即:

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

对应的max_rightmax\_rightmax_right只能在它的右半部分找且不含自身,即:

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

当找到了max_leftmax\_leftmax_leftmax_rightmax\_rightmax_right,按照前面的分析,自然就可以得到该位置的接水量。由于两端不可能接雨水,因此,讨论的范围为[1,len(height)−1)[1, len(height) - 1)[1,len(height)1)。下面逐步的通过图示的方式理解下上述的流程。

  • hieght[1]hieght[1]hieght[1]:当前位置为1,max_left=0max\_left=0max_left=0max_right=3max\_right=3max_right=3,接水量为0


    在这里插入图片描述

  • hieght[2]hieght[2]hieght[2]:当前位置为0,max_left=1max\_left=1max_left=1max_right=3max\_right=3max_right=3,接水量为1


    在这里插入图片描述

  • hieght[3]hieght[3]hieght[3]:当前位置为12,max_left=1max\_left=1max_left=1max_right=3max\_right=3max_right=3,接水量为0


    在这里插入图片描述

  • hieght[4]hieght[4]hieght[4]:当前位置为2,max_left=1max\_left=1max_left=1max_right=3max\_right=3max_right=3,接水量为0


    在这里插入图片描述

  • hieght[5]hieght[5]hieght[5]:当前位置为1,max_left=2max\_left=2max_left=2max_right=3max\_right=3max_right=3,接水量为1


    在这里插入图片描述

  • hieght[6]hieght[6]hieght[6]:当前位置为0,max_left=2max\_left=2max_left=2max_right=3max\_right=3max_right=3,接水量为2


    在这里插入图片描述

  • hieght[7]hieght[7]hieght[7]:当前位置为1,max_left=2max\_left=2max_left=2max_right=3max\_right=3max_right=3,接水量为1


    在这里插入图片描述

  • hieght[10]hieght[10]hieght[10]:当前位置为1,max_left=3max\_left=3max_left=3max_right=2max\_right=2max_right=2,接水量为0


    在这里插入图片描述

  • hieght[11]hieght[11]hieght[11]:当前位置为2,max_left=3max\_left=3max_left=3max_right=1max\_right=1max_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;
    }
}

仔细分析,这种解法需要每个位置都遍历一次给定的数组找max_leftmax\_leftmax_leftmax_rightmax\_rightmax_right,时间复杂度为O(N2)O(N^2)O(N2)。由于过程中没有使用额外的存储空间,空间复杂度为O(1)O(1)O(1)

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


在这里插入图片描述

那么如何找每个位置的max_leftmax\_leftmax_leftmax_rightmax\_rightmax_right?假设从左往右max_leftmax\_leftmax_left,当前遍历到了height[i]height[i]height[i],那么它对应的max_left[i]max\_left[i]max_left[i]取决于height[i−1]height[i-1]height[i1]height[i−1]height[i-1]height[i1]位置对应的max_left[i−1]max\_left[i-1]max_left[i1],而且max_left[i]max\_left[i]max_left[i]的值是两者的最大值。

max_rightmax\_rightmax_right数组的值取决于每个位置右半部分的情况,因此需要从右往左遍历,对应的原则是max_right[i]=max⁡(max_right[i+1],height[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),那么能否不用额外的存储空间完成呢

仔细分析上面的解法可以发现,对于height[i]height[i]height[i]来说,它只关心它对应的max_leftmax\_leftmax_left,而不关心曾经出现过的max_leftmax\_leftmax_left。同样对于max_rightmax\_rightmax_right来说,也只关心它对应的max_rightmax\_rightmax_right,而不关心曾经的max_rightmax\_rightmax_right。因此,我们可以使用两个变量来记录目前最大的max_leftmax\_leftmax_leftmax_rightmax\_rightmax_right。而两端位置的遍历,可以使用指针leftleftleftrightrightright进行记录。

而且对于leftleftleft来说,它对应的max_leftmax\_leftmax_left是不会在变化的,而此时的max_rightmax\_rightmax_right却不一定。因为,leftleftleftmax_rightmax\_rightmax_right对应索引位置之间可能存在更大的值。同样,对于rightrightright来说,max_rightmax\_rightmax_right是不会变化的,但是max_leftmax\_leftmax_left却可能变化。但是,只要max_left<max_rightmax\_left < max\_rightmax_left<max_right成立,即时出现更大的max_rightmax\_rightmax_right,根据接水的原则可知,它不会影响当前的结果。同理,只要max_right>max_leftmax\_right > max\_leftmax_right>max_left成立,即时出现更大的max_leftmax\_leftmax_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;
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值