leetcode 面试经典 150 题(16/150) 42.接雨水

135 阅读8分钟

题目描述

给定 n 个非负整数表示柱子的高度图,计算这些柱子能接住的雨水量。

示例 1:

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:蓝色部分表示雨水,总雨水量为 6

示例 2:

输入:height = [4,2,0,3,2,5]
输出:9

如何计算储水量?

💧 从最简单的例子开始:三根柱子

首先,我们来看一个最最简单的情形:只有三根柱子,形成一个“U”型结构,例如高度数组为 [2, 0, 2]

在这个例子中:

  • 最左边的柱子高度是 2。
  • 中间的柱子高度是 0。
  • 最右边的柱子高度是 2。 可以清晰地看到,中间高度为 0 的区域会被左右两边高度为 2 的柱子“围”起来,形成一个可以储水的空间。

计算积水量

  1. 确定“水面高度”: 水面高度是由左右两边较矮的柱子决定的。在这个例子中,左右两边的柱子高度都是 2,所以水面高度就是 2。
  2. 计算“储水高度”: 对于中间的柱子,它的高度是 0。 因此,它可以储水的高度就是 水面高度 - 柱子自身高度 = 2 - 0 = 2。
  3. 计算总积水量: 由于只有一个宽度单位的“坑”,所以总积水量就是 储水高度 * 宽度 = 2 * 1 = 2。 因此,对于输入 [2, 0, 2],接雨水量为 2

现在,稍微增加一点复杂度,改变中间柱子的高度,来看看积水量如何变化。 [2, 1, 2]

在这个例子中:

  • 最左边的柱子高度是 2。
  • 中间的柱子高度是 1。
  • 最右边的柱子高度是 2。

计算积水量:

  1. 水面高度: 左右两边“墙”的高度仍然都是 2,所以水面高度还是 2。
  2. 储水高度: 中间柱子高度是 1。 储水高度 = 水面高度 - 柱子自身高度 = 2 - 1 = 1。
  3. 总积水量: 1 * 1 = 1。

因此,对于输入 [2, 1, 2],接雨水量为 1。当中间柱子高度升高时,积水量减少了。


继续增加复杂度,将左右两边的柱子高度变为不一致,看看积水量的变化。[3, 0, 2]

在这个例子中:

  • 最左边的柱子高度是 3。
  • 中间的柱子高度是 0。
  • 最右边的柱子高度是 2。

计算积水量:

  1. 水面高度: 这次左右两边的“墙”高度不一样了,分别是 3 和 2。 水面高度由较矮的“墙”决定,所以水面高度是 2
  2. 储水高度: 中间柱子高度是 0。 储水高度 = 水面高度 - 柱子自身高度 = 2 - 0 = 2。
  3. 总积水量: 2 * 1 = 2。

对于输入 [3, 0, 2],接雨水量为 2。虽然左边的墙更高了,但水面高度仍然被右边较矮的墙限制了。


结合上面两种,接着计算 [3, 1, 2]的情况。

在这个例子中:

  • 最左边的柱子高度是 3。
  • 中间的柱子高度是 1。
  • 最右边的柱子高度是 2。

计算积水量:

  1. 水面高度: 左右“墙”高度分别是 3 和 2, 较矮的是 2,所以水面高度为 2
  2. 储水高度: 中间柱子高度是 1。 储水高度 = 水面高度 - 柱子自身高度 = 2 - 1 = 1。
  3. 总积水量: 1 * 1 = 1。

对于输入 [3, 1, 2],接雨水量为 1

从上面的例子,我们能够总结到:

  • 水面高度由左右柱子中较矮的一方决定。
  • 每个位置的积水量是水面高度与该位置柱子高度之差。

扩展到更复杂的例子

现在,来看题目描述的例子,数组为[0,1,0,2,1,0,1,3,2,1,2,1] 为了计算总的积水量,我们可以把每个“水坑”的积水量分别计算出来,然后加总。

  1. 第一个 “坑” (索引 2 的位置,高度为 0):
    • 左边的“墙” (到索引 1 为止) 最高高度是 1。
    • 右边的“墙” (从索引 3 开始) 最高高度是 3 (实际上是索引 7 的柱子)。
    • 较矮的“墙”高度是 1。
    • 储水高度 = 1 - 0 = 1。
    • 积水量 = 1 * 1 = 1。
  2. 第二个 “坑” (索引 4 的位置,高度为 1):
    • 左边的“墙” (到索引 3 为止) 最高高度是 2。
    • 右边的“墙” (从索引 5 开始) 最高高度是 3。
    • 较矮的“墙”高度是 2。
    • 储水高度 = 2 - 1 = 1。 实际上,从图中看,这个位置可以储水的高度应该是 2 - 1 = 1, 因为右边墙更高。 我们稍后会修正这个思考方式。 实际上,这个位置的储水高度应该由 min(左边最高墙, 右边最高墙) - 柱子高度 决定。 所以这里应该是 min(2, 3) - 1 = 2 - 1 = 1。 图中可能视觉效果有些偏差。
  3. 第三个 “坑” (索引 5 的位置,高度为 0):
    • 左边的“墙” (到索引 4 为止) 最高高度是 2。
    • 右边的“墙” (从索引 6 开始) 最高高度是 3。
    • 较矮的“墙”高度是 2。
    • 储水高度 = 2 - 0 = 2。
    • 积水量 = 2 * 1 = 2。
  4. 第四个 “坑” (索引 6 的位置,高度为 1):
    • 左边的“墙” (到索引 5 为止) 最高高度是 2。
    • 右边的“墙” (从索引 7 开始) 最高高度是 3。
    • 较矮的“墙”高度是 2。
    • 储水高度 = 2 - 1 = 1。
    • 积水量 = 1 * 1 = 1。
  5. 第五个 “坑” (索引 9 的位置,高度为 1):
    • 左边的“墙” (到索引 8 为止) 最高高度是 3。
    • 右边的“墙” (从索引 10 开始) 最高高度是 2 (索引 10 的柱子)。
    • 较矮的“墙”高度是 2。
    • 储水高度 = 2 - 1 = 1。
    • 积水量 = 1 * 1 = 1。
  6. 第六个 “坑” (索引 11 的位置,高度为 1):
    • 注意,索引 11 是数组的最后一个元素,它的右边没有墙了,因此这个位置不能积水。 或者,如果非要找右边的墙,可以认为右边没有更高的墙,右边最高的墙就是自身高度 1。 这样 min(左边最高墙, 右边最高墙) = min(2, 1) = 11 - height[11] = 1 - 1 = 0, 积水量为 0。

总积水量 = 1 + 1 + 2 + 1 + 1 + 0 = 6,与示例给出的答案 6 吻合。 从这个例子中,能够总结:

  • 对于更复杂的情况,我们仍然可以逐个位置计算积水量。
  • 关键仍然是找到每个位置左右两侧的“墙”的最大高度,并取较小值作为水面高度。
  • 总积水量是每个位置积水量的总和。

算法思路推导

通过上面的分析,我们已经有了计算每个位置积水量的基本方法。 然而,当我们仔细审视“逐列求雨水”的流程时,会发现一个效率上的瓶颈:为了计算每个柱子上方的积水,我们都不得不重复扫描左右两侧的所有柱子,去寻找 max_leftmax_right。 设想一下,如果柱子数量非常庞大,这种重复扫描的代价就会变得相当可观,算法的效率也会因此大打折扣,这时,“双指针算法” 便应运而生。双指针算法的精妙之处在于,它动态地维护了左右两边的“最高墙”,避免了重复扫描。相反,它采用了左右夹逼,动态更新的策略,巧妙地避免了重复计算,将寻找最高墙的过程融入到指针移动的过程中。

在双指针算法中,leftMaxrightMax 不再是针对每个柱子重新计算的局部最大值,而是全局扫描过程中的动态最大值

  • leftMax:记录的是从数组最左端到当前 left 指针位置所遇到的最大高度,不断更新他们所见过的最高的高度记录。
  • rightMax:记录的是从数组最右端到当前 right 指针位置所遇到的最大高度。 同理,右侧也在不断更新最高记录。

它们随着指针的移动而不断更新,始终反映着从两侧边界到当前位置的最高“墙”的高度信息

if leftMax < rightMax 条件: 当 if leftMax < rightMax 条件成立时,意味着当前左侧的“水位标尺” leftMax 低于右侧的 rightMax。 这时能做出一个关键的判断: 对于当前 left 指针所指的位置,其积水高度完全可以由 leftMax 来界定。

else 分支:

  • else 分支 (leftMax >= rightMax) 的逻辑与 if 分支完全对称。 当 rightMax 小于等于 leftMax 时,我们判定当前 right 指针位置的积水高度由 rightMax 决定,并移动右指针 right。 原理与 if 分支完全一致,只不过方向相反。

核心思想

  • 左右最大值:维护左右两侧的最大高度 leftMaxrightMax
  • 动态更新:根据当前左右最大值的大小关系,决定移动左指针还是右指针,并累加雨水量。

复杂度分析

  • 时间复杂度:O(n),仅需一次遍历。
  • 空间复杂度:O(1),仅使用常量空间。

代码实现

func trap(height []int) int {
    water := 0
    left := 0
    right := len(height) - 1
    leftMax := height[left]
    rightMax := height[right]

    for left < right {
        if leftMax < rightMax {
            left++
            leftMax = max(leftMax, height[left])
            water += leftMax - height[left]
        } else {
            right--
            rightMax = max(rightMax, height[right])
            water += rightMax - height[right]
        }
    }
    return water
}