42. 接雨水(Trapping Rain Water)

难度:困难 | 主题:对撞双指针 / DP

题目

给定 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 单位雨水
 

思路

先讲个故事:操场的排水沟

操场有一段高低不平的围墙(柱子),下雨后凹槽里会积水。

你站在墙壁两端。你只关心一个事:每个凹坑能积多少水?

直觉告诉你:一个位置能不能积水,取决于它左右两边谁更高——准确说,取决于左右最高墙中较矮的那一面。水往低处流,高过两边墙的水会溢出去。

 
位置 i 的积水量 = min(左边最高墙, 右边最高墙) - height[i]
 

引导式推导:从三趟遍历到一趟

第 1 步:最朴素的方法——三趟遍历

对每个位置 i,分别往左和往右找最高墙。

 
leftMax[i] = max(height[0..i])      # 从左往右
 
rightMax[i] = max(height[i..n-1])   # 从右往左
 
ans[i] = min(leftMax[i], rightMax[i]) - height[i]
 

三趟遍历,O(n) 时间 + O(n) 空间。

第 2 步:双指针——为何能砍空间?

能不能一次遍历,不存数组?

关键在于:当 height[l] < height[r] 时,当前左指针位置的左边最高(leftMax)一定小于等于右边最高(rightMax)。

为什么?因为 rightMax ≥ height[r] > height[l],而 leftMax 最多等于 height[l](或者更大一点),所以 leftMax ≤ rightMax 成立。

 
height[l] < height[r]  ⇒  leftMax ≤ rightMax
 

这不就够了吗?左指针位置的积水量 = leftMax - height[l],不需要精确知道右边的最大值。同理,当 height[l] ≥ height[r] 时,用 rightMax - height[r] 算右指针位置。


graph TD

    subgraph 双指针夹逼

        L["l → ..."] -->|"height[l] < height[r]"| CalcL["ans += leftMax - height[l]<br/>l++"]

        CalcL --> L

        L -->|"height[l] ≥ height[r]"| CalcR["ans += rightMax - height[r]<br/>r--"]

        CalcR --> L

    end

第 3 步:为什么移动矮的那边?

因为矮的那边水量已经确定了,不需要等对面的最高值更新。移动高的那边也改变不了矮边的结果。所以谁矮就动谁——这叫短板决定论


三层递进


graph LR

    A["三趟遍历<br/>O(n) 时间 O(n) 空间"]

    B["双指针<br/>O(n) 时间 O(1) 空间"]

    C["单调栈<br/>O(n) 时间 O(n) 空间"]

    A -->|"砍掉两个数组,实时更新 max"| B

    A -->|"维护递减栈,遇高算凹槽"| C

双指针是最优解法——时间 O(n)、空间 O(1)。直接从双指针开始讲,不要先写三趟再优化,显得你没想清楚。

单调栈是另一种思路:维护递减栈,遇到更高的柱子就弹出栈顶计算凹槽水量。按行计算,适合理解积水形成的「分层」过程。实践中作为对比方案提即可。


代码

 
def trap(self, height):
 
    l, r = 0, len(height) - 1         # 左右指针,从两端夹逼
 
    left_max = right_max = 0           # 当前左右两侧最大高度
 
    ans = 0
 
    while l < r:
 
        # 先更新当前边界最高值
 
        left_max = max(left_max, height[l])
 
        right_max = max(right_max, height[r])
 
        if height[l] < height[r]:
 
            # 左边矮 → 左边水量确定,用 left_max 算
 
            ans += left_max - height[l]
 
            l += 1
 
        else:
 
            # 右边矮(或相等)→ 右边水量确定
 
            ans += right_max - height[r]
 
            r -= 1
 
    return ans
 

复杂度

方法时间空间
双指针O(n)O(1)
DP(左右数组)O(n)O(n)
单调栈O(n)O(n)

实战考量

频率分析

出现在:字节/阿里/腾讯高频困难题。这道题考察对双指针的理解深度——为什么移动矮的,以及为什么不需要精确知道两边最高

延伸思考

Q:为什么 while l < r 不是 <=

A:l r 时只剩一根柱子,两边没有空间围水,积水量为 0。所以到 l r 就结束。

Q:为什么先更新 left_max/right_max 再算积水?

A:当前柱子的高度需要参与计算。如果不先更新,当 height[l] 本身就是当前最大值时,left_max - height[l] = 0(柱子本身不积水),正确。如果先算再更新,那 left_max 还是旧的,可能少算或多算。

Q:用 DP 怎么做?

A:先从左到右扫一遍得到 leftMax 数组(leftMax[i] = max(leftMax[i-1], height[i])),再从右到左扫得到 rightMax,然后遍历算 min(leftMax[i], rightMax[i]) - height[i]。代码更直白,但多两个数组。

Q:用单调栈怎么做?

A:维护一个递减栈。遍历高度,当 height[i] > height[stack[-1]] 时,弹出栈顶(凹槽底部),用当前高度和栈内下一个高度算凹槽宽度和高度,累计水量。按行计算,更符合「积水是一层一层」的直观理解。

Q:「11盛最多水的容器」和这题什么关系?

A:姐妹题。都是对撞双指针 + 移动矮的。但 11 题是求最大面积(找两个柱子围的最大矩形),这题是求累计水量(所有凹槽的总和)。11 题的水不会溢出,这题每个位置都要算。

易错点

  • while l < r 不是 <=

  • 先更新 left_max/right_max,再算积水,最后移动指针

  • 双指针的核心不完全是「移动矮的」,而是移动水量已确定的那个


生活类比

接雨水 → 操场的排水沟 → 短板决定论

想象两边各有一块挡板。水能积多少,取决于矮的那个挡板。

你不需要知道远处多高,只需要知道眼前这个坑的矮边在哪。

移动矮边,水量确定,从不犹豫。

核心就是:谁矮动谁,短板即答案。


相关题目

题目关系
11盛最多水的容器姐妹题,对撞双指针 + 移动矮的,本题累计水量,11 求最大面积

→ 返回题单:LeetCode学习路线图 > 一、数组与哈希(含双指针、矩阵)

速记卡(面试闪卡)

Q1:一句话讲清「42. 接雨水(Trapping Rain Water)」到底是什么?

A:接雨水用对撞双指针,按”短板决定论”在 O(1) 空间算累计水量。

Q2:一、题目与核心直觉:左右矮边决定水量 —— 怎么理解?

A:像操场排水沟的凹槽:一个位置能积多少水,取决于左右最高墙中较矮的那面——水往低处流,高过两边就溢出。英文:Trapping Rain Water。

Q3:二、从三趟遍历到双指针 —— 怎么理解?

A:像先记全班身高再算,不如实时更新:朴素法存 leftMax/rightMax 两数组 O(n) 空间;双指针边走边更新 max,谁矮算谁、省掉数组。英文:Two Pointers。

Q4:三、为什么移动矮的那边 —— 怎么理解?

A:像短板已定的木桶:当 height[l]<height[r] 时左边界水量已锁定,动高的也改不了矮边结果——所以”谁矮动谁”,移动水量已确定的那个。英文:Short-Board Rule。

Q5:四、单调栈与其他解法 —— 怎么理解?

A:像另一种分层量水法:单调栈遇高弹栈按”行”算凹槽,直观但 O(n) 空间;面试直接讲双指针 O(n)/O(1) 最稳,栈作对比提一句。英文:Monotonic Stack。

Q6:核心速记主线有哪些?

  • 核心公式:积水 = min(左最高, 右最高) - 当前高度

  • 双指针:谁矮动谁,水量已确定才移动

  • 对比:三趟遍历 O(n)空间 / 双指针 O(1) / 单调栈按行

  • 易错:先更新 max 再算、while 用 <、移动已定边

口诀

A:接雨水看矮挡板,

谁矮动谁水算完;

双针夹逼 O(1),

短板决定不犯难。

相关链接