• LeetCode (42): Trapping Rain Water


    链接: https://leetcode.com/problems/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.

    For example, 
    Given [0,1,0,2,1,0,1,3,2,1,2,1], return 6.

    【中文描述】

    给定n个非负数,想像它们代表了n堵墙,墙的高度就是Ni, 现在想像下了一场雨,让求这些墙能保存多少水?如图:

    ————————————————————————————————————————————————————————————

     

    【初始思路】

    这个题挺有意思, 所以也没看discuss, 完全自己硬刚出来的。这个题其实不难,主要是考验思维的缜密程度,然后逐段逐情况分析即可。

    我一开始的思路是(brutle的就不讨论了吧),用i遍历数组,遇到比i高的,就停下来,然后算里面的水量,然后更新i。

    这是最基本的思路了,但是,里面有巨量的细节需要考虑:

        细节1: 如果i下一位比i高或和i一样高,显然i这堵墙就没用了, continue;

    好了,i 的下一堵墙肯定低于 i 墙,这个时候,我使用了动态规划的思想,不管三七二十一,先开始按照 i 墙的高度算水量,给一个变量j = i+1, j一直移动到结尾。

    那么在 j 移动到结尾的过程中,有几种可能性:

    (1)遇到了某一堵墙比 i 墙要高, 真是太好了, 那么计算到这个位置的时候, 这段区间内的水量就是咱们动态规划计算出来的水量, result加入这个值就可以了。然后j也不用再往后看了, i=j,continue即可, 可以看下图帮助理解这种情况, 浅绿色的面积 == maxPoten, 这就是留水量。这个值加入result后, i移动到j,然后继续用j往后找就行了。

                                         

    (2)从头到尾,j都没有遇到比i还高的墙,那怎么办?这个时候,最简单的办法是,i 直接continue。 但是,这样的话,我们刚才算过的值全都白算了,而且这样算,时间复杂度肯定就接近O(n2)了。所以我根本没实现这个思路(虽然这个思路是最简单的,面试时候可以直接这么写);

    (2改)我的思路是,已经算的不能白费,怎么办呢?既然比 i 墙高或者等于 i 墙的没找到,那我们就争取找到比 i 低的最远处的一堵墙。如果最坏情况下,没找到比 i 墙高或者等于 i 墙的, 我们还可以用比 i 墙稍低一点的这个墙来补偿,刚才做的计算也不会白费。  并且!!最重要的是, 这样计算完后, i可以直接跳到这堵稍低点的墙的位置,时间复杂度大大降低。最优情况可以接近O(n)!

     好了, 这样的话,我们在计算j->结尾的这个过程中, 有几个变量需要实时计算:

    maxPoten, 代表了最大可能留水量。当找到比i墙高或者等于i墙的, 这个值就是留水量, 绝对不会错!

    lower, 代表了没找到理想墙,但是找到了比 i 墙稍低一点的墙, 那么在这种情况下, lower墙决定的留水量就是这个区间内的留水量,绝对不会错!

    lowerPoten,代表由lower墙决定的留水量。 并且,lowerPoten和maxPoten之间有算术关系,稍后我们讨论这个关系,并且给出公式。

    这些是否足够了?我们来看看下面的图,帮助理解这种情况下可能的问题:

                                            

    如上图, 找到lower后,实际的存水量应该是lowerPoten,那么lowerPoten怎么算出来。maxPoten -(两墙高差)*(两墙距离) = lowerPoten?光凭抽象理解,很容易得出这样一个错误的式子,但是看上面图就一目了然了, 细节2: lower后面的面积,也需要减去!正如图上标示的一样,这个面积怎么算?

    我们根据lower的更新机制来看, 这个面积其实和lower息息相关,我们实时计算的时候,除了实时计算maxPoten,再计算这个面积,然后每次求出新的lower的时候,这个面积归零。最终,我们就可以求得这个面积了。

    所以,我们还需要一个变量: lowerBehind.

    那么, lowerPoten = maxPoten - lowerBehind - (两墙高差)*(两墙距离)。

    这样是否就OK了?答案是否定的,我们来看下面的图:

                                       

    上图可以很明显看出来, 由于左上角橘红色区块的存在,之前的公式就是错误的了: lowerPoten = maxPoten - lowerBehind - (两墙高差)*(两墙距离)。

    因为显然,细节3: maxPoten需要减掉step上的水体积,然后再减(lower和step之间的间距) * (两墙高差) 才能得到正确答案。

    所以,需要考虑下怎么算step上的水量。

    思考一下step为何会产生,从图上可以看出来,因为step高度>=lower墙的高度。step的确定看似简单:j从i+1位开始往后推移的时候,遇到比lower高且和i墙紧邻的都是step。但是实现的时候这个想法就不好实现了,因为lower还没有产生,我怎么知道当前这个墙是不是step。 所以,我的思路是:

          只要 j 墙紧邻 i 墙,就把 j 墙先算作step,然后存入一个list里。

    怎么确定紧邻?简单,给一个boolean的紧邻标记=true, 只要当前 j 墙不为0且紧邻标记为true,那么当前 j 墙肯定紧邻 i 墙。如果一旦 j 墙==0了,紧邻标志置为false即可。

    在最终确定了lower后,我们再遍历这个list,把从左往右最后一个大于lower的墙标记为最右step墙。 那么 step1, step2, .. stepn的水量加起来就是上图中的step水量。同时,因为已经算出了最靠右的step墙,那么lower和这个墙之间的差距就可以用来算lowerPoten了。

    到此,我们得出最后的公式:

          lowerPoten = maxPoten - lowerBehind - stepWater  -(两墙高差)*(lower - 最右step墙) 

     

    最后整个算法最核心的点:lower,怎么算?

    首先,lower肯定是从最小往最大去更新。 那么lower最开始=0, 然后遇到比当前lower大的,lower就更新。但是这样做的话,有一种情况就无法解决了。看下图:

                          

    根据上面描述,那么遇到这种递减数列到结束的情况, lower就会在i + 1的位置。这显然是不合理的。因为这样的话,这种情况下也能留水!那么lower应该怎么选?

    显然,lower只要在数列不是递减的情况下,才会至少找到一个。如果数列递减,lower肯定不能找到。

    所以,lower的更新机制,除了上面提到的之外,还需要加上,细节4: j 墙 > j - 1墙的情况下, lower开始更新。一旦出现了j墙>j-1墙的情况, 肯定不是递减数列了,那么就可以更新lower了。

    到此,整个算法里的细节就都分析到了,综上,可以写出代码。

     

    【Show me the Code!!!】

     1 if (height == null || height.length == 0) return 0;
     2 
     3         int result = 0;
     4         int i = 0;
     5         while (i < height.length) {
     6             //每次i移动后, 几个变量要归0
     7             int maxPoten = 0;
     8             int lowerPoten = 0;
     9             int lower = -1;
    10             int lowerBehind = 0;//lower后的总水量,必要时候要减掉,每次lower更新后,此值更新到0,并重新累积
    11             if (i < height.length  - 1 && height[i] <= height[i + 1]) {
    12                 i++;
    13                 continue;//当前i比后一个矮, 直接continue, 细节1
    14             }
    15             List<Integer> union = new ArrayList<Integer>();//保存和i墙连续的低墙
    16             boolean isUnion = true;
    17             int j  = i + 1;
    18             while (j < height.length) {
    19                 if (height[j] >= height[i]) {
    20                     //找到了比height[i]还高或者一样高的, 直接break;
    21                     //当前的maxPoten就是临时结果,直接加入result即可
    22                     break;
    23                 }
    24                 if (lower == -1 && height[j] > height[j - 1]) {
    25                     //第一次出现了升序, lower更新到第一个位置, 细节4
    26                     lower = j;
    27                 }
    28                 if (height[j] == 0) {
    29                     isUnion = false;
    30                 }
    31                 maxPoten += height[i] - height[j];//每一步都要计算maxPoten
    32                 lowerBehind += height[i] - height[j];//细节2, lower后的面积需要计算出来,必要时候要减去
    33                 if (isUnion) {
    34                     union.add(j);//最终计算的时候, union部分也需要考虑
    35                 }
    36                 if (lower != -1 && height[j] >= height[lower]) {
    37                     lower = j;//更新lower到最远,且高度仅次于height[i]的位置
    38                     //每次更新lower后,lowerBehind从头算
    39                     lowerBehind = 0;
    40                 }
    41                 j++;
    42             }
    43             // 有2个可能性:
    44             // (1) break出来的, 那么maxPoten直接加入result, continue
    45             // (2) j遍历完了整个数组, 说明maxPoten不可用, 那么计算lowerPoten
    46             if (j == height.length) {
    47                 //说明是正常遍历完的, lower起作用了
    48                 //计算lowerPoten
    49                 //先算起初台阶的存水,这部分要区分对待
    50                 if (lower == -1) {
    51                     //说明也没找到lower, 说明就是一直降的台阶, 循环直接结束
    52                     break;
    53                 }
    54                 int stepWater = 0;
    55                 int k = 0;
    56                 if (union.size() > 0) {//计算台阶
    57                     while (k < union.size() && height[union.get(k)] > height[lower]) {
    58                         stepWater += height[i] - height[i + k + 1];//细节3, 计算step上的水量
    59                         k++;
    60                     }
    61                 }
    62                 //此外, maxPoten应截止到lower, lower后的水不能再算进去.
    63                 lowerPoten = maxPoten - stepWater - lowerBehind - (height[i] - height[lower]) * (lower - (i + k));
    64                 result += lowerPoten;
    65                 i = lower; // i更新到lower位置
    66             } else {
    67                 //说明是break出来的
    68                 result += maxPoten;
    69                 i = j; //i移动到j的位置
    70             }
    71         }
    72 
    73         return result;
    74     }
    trap

    代码有点长,但是效率绝对高,leetCode上跑一遍全部用例,只用了5ms。

    时间复杂度上来看,由于每次算出lower 或者 找到比 i 墙还高的墙之后, i 直接移到了lower或更高的墙去。 所以理想情况下是O(n)的复杂度。平均摊还上来看,也比较接近O(n)。

     

     

     

  • 相关阅读:
    Three.js入门和搭建HelloWorld
    CentOS中使用Docker来部署Tomcat
    CentOS中使用Docker来部署Nginx
    Windows中通过bat定时执行命令和mysqldump实现数据库备份
    Geoserver在Linux上的安装(图文教程)
    Linux上怎样停止服务(jar包)、查看进程路径、关闭指定端口
    Java中怎样监测局域网内ip是否互通,类似ping
    Android和JS之间互相调用方法并传递参数
    Android中使用WebView加载本地html并支持运行JS代码和支持缩放
    ARC072C
  • 原文地址:https://www.cnblogs.com/lupx/p/leetcode-42.html
Copyright © 2020-2023  润新知