对接雨水问题的思考

algorithm
Author

0warning0error

Published

June 29, 2024

今天回顾了一道题目

给定 n 个非负整数表示每个宽度为 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 个单位的雨水(蓝色部分表示雨水)。 

解决这个问题的想法很简单,求出每个柱子上方有多少雨水,最后累加即可。而一个柱子上方有多少雨水,取决于自身的高度,以及左边柱子高度的最大值和右边柱子高度的最大值。

这样,动态规划的方法就不难理解,从左往右得到每根柱子左侧柱子高度的最大值,再从右往左得到每根柱子右侧柱子高度的最大值,最后从左往右计算每个柱子上方有多少雨水。代码如下

class Solution:
    def trap(self, height: List[int]) -> int:
        if not height:
            return 0 # 没有柱子存不了雨水
        
        n = len(height)
        leftMax = [height[0]] + [0] * (n - 1)
        for i in range(1, n):
            leftMax[i] = max(leftMax[i - 1], height[i]) #从左往右得到左侧柱子高度的最大值

        rightMax = [0] * (n - 1) + [height[n - 1]]
        for i in range(n - 2, -1, -1):
            rightMax[i] = max(rightMax[i + 1], height[i]) #从右往左得到每根柱子右侧柱子高度的最大值

        ans = sum(min(leftMax[i], rightMax[i]) - height[i] for i in range(n))
        return ans

但这种方式效率很低,其实回顾思路发现,一个柱子上方的雨水,取决于左边柱子高度的最大值和右边柱子高度的最大值。如果知道哪测比较高,那就只会用到另外一侧的最大值。所以可以改用双指针用来代替前面的数组。代码如下:

class Solution:
    def trap(self, height: List[int]) -> int:
        ans = 0
        left, right = 0, len(height) - 1
        leftMax = rightMax = 0

        while left < right:
            leftMax = max(leftMax, height[left])
            rightMax = max(rightMax, height[right])
            if height[left] < height[right]: # 如果无法理解这个条件,可以换成 leftMax < rightMax
                ans += leftMax - height[left]
                left += 1
            else:
                ans += rightMax - height[right]
                right -= 1
        
        return ans

当时阅读leetcode官方的题解有如下的描述

如果 height[left] ≥ height[right],则必有 leftMax ≥ rightMax

这个推论对我而言并不容易理解,但可以使用结构归纳法证明。

证明

假设有height 的数组,初始状态是left, right = 0, len(height) - 1。很显然,leftMax是height[0],rightMax是height[-1],一定成立。

然后关键的就是递推情况。假设 现在是left == l, right == r,而height[l] ≥ height[r],需要证明此时leftMax ≥ rightMax。

达到left == lright == r的上一个状态有2种。

right == r + 1 而来

在这种情况下,不难得出leftMax ≥ min(height[r + 1],…,height[n - 1])。而height[l] ≥ height[r] , 所以leftMax ≥ height[r],最后leftMax ≥ rightMax。

left == l - 1 而来

此时根据推论,min(height[0],…,height[l - 1]) ≤ rightMax,而height[l] ≥ height[r]。

这个状态下其实 height[r] == rightMax。我们可以用反证法证明。

如果height[r] != rightMax,那么rightMax > height[r], min(height[0],...,height[l - 1]) ≤ rightMax == min(height[r + 1],...,height[n - 1]). 此时right 应该大于 r,在右侧柱子高度最大值所在的索引(否则,之前right在r的时候应该继续往左移动),矛盾。

证明height[r] == rightMax以后,由于height[l] ≥ height[r],height[l] == leftMax ≥ height[r] == rightMax。证毕。