🌧️ 接雨水(Trapping Rain Water):从直觉到最优解的完整思考路径
题目来源 :LeetCode #42
难度 :Hard
关键词 :双指针|动态规划|单调栈|前缀最大值
一句话理解:给定一排柱子的高度,问下雨后能接住多少单位的雨水?
如果你第一次看到这道题时一脸茫然:"水怎么算?凭感觉?"------别担心,你不是一个人。但其实,只要抓住一个核心思想,这道"难题"就会变得异常清晰。
今天,我将带你一步步拆解「接雨水」问题,从最朴素的暴力法出发,逐步优化到空间 O(1) 的双指针最优解,并解释为什么这样想是合理的。
一、问题可视化:水到底怎么"接"?
输入示例:
python
height = [0,1,0,2,1,0,1,3,2,1,2,1]
💡 关键观察:
- 水不会从两边流走 → 必须有左右更高的柱子形成"凹槽"
- 某个位置
i能接多少水?
取决于它左边最高的柱子和右边最高的柱子中较矮的那个
✅ 核心公式
pythonwater[i] = max(0, min(left_max[i], right_max[i]) - height[i])
这个公式就是整道题的"灵魂"。
二、方法一:暴力法(O(n²))--- 理解问题的起点
对每个位置 i,我们向左、向右扫描,找出最大高度:
python
def trap(height):
total = 0
n = len(height)
for i in range(n):
left_max = max(height[:i+1])
right_max = max(height[i:])
total += max(0, min(left_max, right_max) - height[i])
return total
代码中left_max = max(height[:i+1]) 和 right_max = max(height[i:]) 的作用:
🎯 目标:对每个位置 i,找出:
- 左边(含自己)最高的柱子 →
left_max - 右边(含自己)最高的柱子 →
right_max
🔍 详细解释:
height[:i+1] 是什么?
- 这是 Python 的切片语法 ,表示从开头到索引
i(包含i) - 因为切片
[a:b]的规则是:左闭右开 → 包含a,不包含b - 所以
[:i+1]= 索引0到i(共i+1个元素)
- ✅ 逻辑简单,容易验证
- ❌ 时间复杂度 O(n²),大数据会超时
📌 价值:帮助我们确认"每个位置独立计算"的思路是正确的。
三、方法二:动态规划(O(n) 时间 + O(n) 空间)--- 预处理优化
既然每次都要找左右最大值,不如提前算好!
步骤:
- 从左往右遍历,记录每个位置左侧(含自身)的最大高度 →
left_max - 从右往左遍历,记录每个位置右侧(含自身)的最大高度 →
right_max - 再遍历一次,用公式累加雨水
python
def trap(height):
if not height: return 0
n = len(height)
left_max = [0] * n
left_max[0] = height[0]
for i in range(1, n):
left_max[i] = max(left_max[i-1], height[i])
right_max = [0] * n
right_max[-1] = height[-1]
for i in range(n-2, -1, -1):
right_max[i] = max(right_max[i+1], height[i])
total = 0
for i in range(n):
total += min(left_max[i], right_max[i]) - height[i]
return total
代码中for i in range(n-2, -1, -1): 是什么意思?
这是 Python 中从后往前遍历的标准写法。
🔧 range(start, stop, step) 规则:
- 从
start开始 - 到
stop之前 停止(不包含stop) - 每次增加
step
所以:
python
range(n-2, -1, -1)
- start = n-2 :从倒数第二个元素开始(索引
n-2) - stop = -1 :停在
-1之前 → 最后一个有效索引是0 - step = -1:每次减 1(反向)
- ✅ 时间 O(n),逻辑清晰
- ✅ 面试中写出这个解法已经能拿高分
- ❌ 需要额外 O(n) 空间存储两个数组
四、方法三:双指针(O(n) 时间 + O(1) 空间)--- 最优解!
🔑 核心洞察:
我们不需要同时知道 left_max 和 right_max !
只要知道较小的那个就够了------因为水高由"短板"决定。
🤔 双指针策略:
- 用两个指针
left和right从两端向中间移动 - 维护
left_max和right_max(当前已知的最大值) - 如果
height[left] < height[right]:- 说明
left这边是"短板" - 那么
left位置的积水高度只由 left_max 决定(因为右边一定有更高的柱子兜底!) - 更新
left_max或累加雨水,然后left++
- 说明
- 否则,处理
right侧
✅ 代码实现(极简):
python
def trap(height):
if not height: return 0
left, right = 0, len(height) - 1
left_max = right_max = 0
total = 0
while left < right:
if height[left] < height[right]:
if height[left] >= left_max:
left_max = height[left]
else:
total += left_max - height[left]
left += 1
else:
if height[right] >= right_max:
right_max = height[right]
else:
total += right_max - height[right]
right -= 1
return total
- ✅ 时间 O(n),空间 O(1)
- ✅ 一次遍历完成,效率最高
- 💡 面试黄金解法!
五、为什么双指针是安全的?------ 直觉解释
假设 height[left] < height[right]:
- 此时,
right侧至少有一个柱子(就是height[right])比left高 - 所以,
left位置的积水上限不会受右边更远柱子的影响 ------因为已经有height[right]这个"高墙"挡着了 - 因此,只需关心
left左边的历史最大值即可!
这就是"用已知的短板决定当前水量"的精妙之处。
六、总结:四种解法对比
| 方法 | 时间复杂度 | 空间复杂度 | 是否推荐 |
|---|---|---|---|
| 暴力法 | O(n²) | O(1) | 仅用于理解 |
| 动态规划 | O(n) | O(n) | ✅ 面试稳妥 |
| 双指针 | O(n) | O(1) | ✅✅ 最优解,必会 |
| 单调栈 | O(n) | O(n) | 适合变种题(如求积水区域) |
七、我的收获
- 问题建模能力:把"接雨水"转化为"每个位置的积水高度计算"
- 预处理思想 :通过提前计算
left_max/right_max避免重复扫描 - 双指针的高级用法:不只是"找和",还能"维护最值 + 利用对称性"
- 空间优化的艺术:通过逻辑推理,省去不必要的存储