[LeetCode] 接雨水

127 阅读4分钟

这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战

题目

42. 接雨水

解析

这道题也是不知道第多少次做了,还是没有思路,因此再做一次。

先思考一个问题:

  • 如何判断某个位置是否可以蓄水?

条件就是:这个位置的左右都有严格大于这个位置高度的格子。

那么,再思考一个问题:

  • 如何判断某个位置的蓄水量?

在满足上面的条件下,某个位置的蓄水量就是:这个数往左右看的最高的板子的最小值,减去这个位置板子的高度。

那么,只要求得对于位置i,往左右看的最高的板子的高度,就可以轻易求出题目所求的蓄水量。

转化一下,往左看就代表着从这个数开始往左的序列中的最大值,往右看亦然。

那么,将上面的设计转化为所给的即可得以下代码:

public int trap(int[] height){
    int n = height.length;
    //max[i][0]:左最大值,max[i][1]:右最大值
    int[][] max = new int[n][2];
    //需要给边界设定上值,第0个左边肯定是自己最大,第n-1个必然是的右边必然是自己最大
    max[0][1] = max[0][0] = height[0];
    max[n-1][0] = max[n-1][1] = height[n-1];
    //我们依次求取向左边看的最高
    for (int i = 1; i < height.length; i++) {
        max[i][0] = Math.max(max[i-1][0],height[i]);
    }

    //再依次求取右边看的最高
    for(int i = n-2;i>=0;i--){
        max[i][1] = Math.max(max[i+1][1],height[i]);
    }

    //最后求值,某一个位置是否能蓄水,取决于左边看和右边看的最大值之间小的那个
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += Math.min(max[i][0],max[i][1]) - height[i];
    }

    return total;
}

执行用时:2 ms, 在所有 Java 提交中击败了39.17%的用户

内存消耗:40.9 MB, 在所有 Java 提交中击败了5.05%的用户

OK,现在题解AC了,思考一下有没有改进的空间?毕竟这个5.05%的空间复杂度,必然是有更优的解法的。

滚动数组

注意到:

  • 在更新往左看的最大值的时候,后一个的值只由当前位置的值和前一个的值决定;
  • 往右看的时候亦然。

考虑一下:如果我们用的是DP的话,那么这种情况就可以用滚动数组的方式来优化(毕竟只由前一个的值确定)。

那么,思考一下以什么方式滚动?

暂时想不出来如何抽象到使用指针的方式,那么看看是否可以优化到一维数组?

注意到:我们求结果的时候,是以左右最大值的最小值来确定的。

那么,可以优化到只用一个数组,只保存最小值即可。

流程假定还跟上面相同,从左到右,再从右到左。

那么,从右到左的时候,如果maxR<maxL,保存的是maxR;对于左边的数,maxL比maxR大,实际上对于最后的计算是没有意义的,因为只要最小值

代码如下:

public int trap3(int[] height){
    int n = height.length;
    int[] dp = new int[n];
    dp[0] = height[0];
    for (int i = 1;i<n;i++)
        dp[i] = Math.max(dp[i-1],height[i]);
    dp[n-1] = height[n-1];
    for(int i = n-2;i>=0;i--)
        dp[i] = Math.min(Math.max(dp[i+1],height[i]),dp[i]);
    int total = 0;
    for (int i1 = 0; i1 < dp.length; i1++) {
       total += dp[i1] - height[i1];
    }
    return total;
}

执行用时:1 ms, 在所有 Java 提交中击败了79.57%的用户

内存消耗:40.9 MB, 在所有 Java 提交中击败了5.05%的用户

吐槽一下这个统计,明明二维变一维了内存消耗还是一样的就很不科学。。。

在上面的版本的基础上,我们很容易地可以将第二次遍历求右最大值的过程,用一个指针来取代更新。(同时可以不用再遍历dp数组求和):

public int trap4(int[] height){
    int n = height.length;
    int[] dp = new int[n];
    dp[0] = height[0];
    for (int i = 1;i<n;i++)
        dp[i] = Math.max(dp[i-1],height[i]);
    int rMax = height[n-1];
    int total = 0;
    for(int i = n-1;i>=0;i--){
        rMax = Math.max(rMax,height[i]);
        total += Math.min(rMax,dp[i]) - height[i];
    }
    return total;
}

执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户

内存消耗:41.2 MB, 在所有 Java 提交中击败了5.05%的用户

进一步地想,既然单指针可以有,那么双指针呢?

回顾一下上面为什么在从右遍历的时候,可以不用再更新dp表?

  • 因为当计算右最大值的时候,左最大值已经确定了。

但其实,回顾一下trap2中,所做的工作:所求的是左右最大值的小的那一个[1]

因此,如果使用双指针一次遍历,得知右边有比左边更高的板子(不一定要确切地知道是多少,只要知道有即可),那么左边就可以得知:当前位置的蓄水量就变成单相关了:只和左边的最大值有关。

举个抽象的例子:使用双指针,lp为左指针,rp为右指针,lMax为左到lp的最大值,rMax为rp到右的最大值,那么:

  • 如果rMax<lMax,那么即使lp到rp之间有比rMax更大的值,rp位置的蓄水量也仅由rp决定(见[1]).
  • lMax<rMax,依然,此时rp换成lp。
  • lMax=rMax,此时lp到rp之间的蓄水量也同样的由lMax决定。

终止条件也简单,当lp==rp的时候,意味着已经遍历完了所有的板子,此时就剩一个,此时可以得到在该位置的左右最大值,取小的即可。

根据上述思路就可以拟出以下答案:

    //彻底抛弃dp数组
    public int trap5(int[] height){
        int lp = 0 , rp = height.length-1;
        int lMax = height[lp],rMax = height[rp];
        int total = 0;
        while(lp < rp){
            if(lMax>rMax){
                total += rMax - height[rp];
                rp--;
                rMax = Math.max(height[rp],rMax);
            }else{
                total += lMax - height[lp];
                lp++;
                lMax = Math.max(height[lp],lMax);
            }
        }
        return total + Math.min(lMax,rMax) - height[lp];
    }

执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户

内存消耗:41.5 MB, 在所有 Java 提交中击败了5.05%的用户

PS:我是不是黑号啊这个内存消耗越来越大

嗯没事,加个gc看这个版本的空间使用情况的最终结果:

public int trap5(int[] height){
    System.gc();
	//...
}

执行用时:4 ms, 在所有 Java 提交中击败了14.80%的用户

内存消耗:37.8 MB, 在所有 Java 提交中击败了94.28%的用户

嗯,这样就对了。