接雨水--动态规划+优化

0x01.问题

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
在这里插入图片描述上面是由数组 [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

题目及图片来源于Leetcode

Java函数形式:     public int trap(int[] height)

0x02.简单分析–初始思路

初看问题,觉得非常复杂,因为影响水量的因素有很多,不知如何去考虑。那是因为我们还没有确定一个具体的方向,所以会感觉很乱。
我们的第一步就是确定一个方向,按照什么标准去计算?

  • 一种简单粗暴的方法是按照行来计算,对每一行来考虑是否能存水。这种思路判断简单,但是复杂度由最高的高度决定,效率肯定非常低。
  • 另一种容易想到的方法是从每一个格子开始去后面找能存水的地方。这个思路可行,但实在是太复杂,需要考虑非常多的情况,比如一片区域是否已经被计算,是否找到更大的存水空间,需要更新,是否会超出边界等等,所以不推荐。
  • 另外,我们可以从列入手,计算每一列的存水量,这个方法还是比较可行的,且存在非常大的优化空间。
  • 使用栈也是可以的,但可能一开始想不到。

这里介绍第三种思路,按照列去进行计算。

我们再来看第二个问题,如何计算每一列的存水量?

  • 一列的存水量应该和左右的最大高度有关,且根据短板效应,应该取决于左右最大高度中的最小值。

于是我们可以开始简单的去模拟这个过程:

  • 对每一个端点,计算左右最大高度,然后取最小值减去当前高度。

代码如下:

class Solution {
    public int trap(int[] height) {
        int ans=0;
        int n=height.length;
        for(int i=0;i<n;i++){
            int left=0,right=0;
            for(int j=i;j>=0;j--){
                left=Math.max(left,height[j]);
            }
            for(int k=i;k<n;k++){
                right=Math.max(right,height[k]);
            }
            ans+=Math.min(left,right)-height[i];
        }
        return ans;
    }
}
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)

0x03.再分析–优化时间维度

我们可以发现内存循环的每一次循环的目的都只是找出最大值,这里面存在了大量的重复运算,我们可以提前把这些计算出来,然后直接拿来使用,做到循环并行而不是嵌套。

这其实就是动态规划的思路:

  • 转移方程为:
  • left[i]=Math.max(left[i-1],height[i-1]);
  • right[i]=Math.max(right[i+1],height[i+1]);

代码如下:

class Solution {
    public int trap(int[] height) {
        int ans=0;
        int n=height.length;
        int[] left=new int[n];
        int[] right=new int[n];
        for(int i=1;i<n-1;i++){
            left[i]=Math.max(left[i-1],height[i-1]);
        }
        for(int i=n-2;i>=0;i--){
            right[i]=Math.max(right[i+1],height[i+1]);
        }
        for(int i=1;i<n-1;i++){
            int tmp=Math.min(left[i],right[i])-height[i];
            if(tmp>0) ans+=tmp;
        }
        return ans;
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

0x04.dp的空间优化-1

我们从代码中可以看到,其实每次都只使用了dp一个值,按照dp的常规优化,我们可以把left用一个变量替代,right暂时不能,因为遍历的方向不同。

代码如下:

class Solution {
    public int trap(int[] height) {
        int ans=0;
        int n=height.length;
        int left=0;
        int[] right=new int[n];
        for(int i=n-2;i>=0;i--){
            right[i]=Math.max(right[i+1],height[i+1]);
        }
        for(int i=1;i<n-1;i++){
            left=Math.max(left,height[i-1]);
            int tmp=Math.min(left,right[i])-height[i];
            if(tmp>0) ans+=tmp;
        }
        return ans;
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)
  • 这里的O()N实际上比上一段代码是有优化的,省去了一个数组。

0x05.dp的空间优化-2-双指针

我们从上段代码的优化思路可以得到,我们之所以不能优化的原因是因为遍历方向不同,那么,我们可不可以在一段循环中用两种方向遍历呢?

答案是肯定的,我们可以使用双指针,从两端开始遍历,双指针相遇就结束循环,那么问题来了,什么时候正向遍历,什么时候反向遍历?

  • height[left]<height[right]向左端遍历,反之,向右端遍历。
  • 解释:由ans+=Math.min(left,right)-height[i];可以得到,决定遍历方向的是左右最大高度谁最小。
  • 如果一端有更高的条形块(例如右端),积水的高度依赖于当前方向的高度(从左到右)。当我们发现另一侧(右侧)的条形块高度不是最高的,我们则开始从相反的方向遍历(从右到左)。
class Solution {
    public int trap(int[] height) {
        int ans=0;
        int n=height.length;
        int left=0,right=n-1;
        int lmax=0,rmax=0;
        while(left<right){
            if(height[left]<height[right]){
                if(height[left]>lmax) lmax=height[left];
                else ans+=lmax-height[left];
                left++;
            }
            else{
                if(height[right]>rmax) rmax=height[right];
                else ans+=rmax-height[right];
                right--;
            }
        }
        return ans;
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

Leetcode-4.4每日一题打卡完毕!

心情日记:当你并不只为了自己时,你没有退缩的理由!!!

ATFWUS --Writing By 2020–04-04

发布了193 篇原创文章 · 获赞 216 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/ATFWUS/article/details/105306450
今日推荐