1. 问题背景与直观理解
"接雨水"问题是一个经典的算法题目,经常出现在技术面试和编程竞赛中。想象一下,在某个城市下了一场大雨,不同高度的建筑物之间会积攒多少雨水?这就是我们需要计算的问题。
这个问题看似简单,但蕴含着丰富的算法思想。我第一次遇到这个问题是在一次面试中,当时给出的解法虽然能运行,但效率并不理想。后来经过深入研究,才发现其中蕴含着多种解题思路,每种方法都有其独特的视角和优化空间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题描述与形式化定义
给定n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。例如,给定高度数组[0,1,0,2,1,0,1,3,2,1,2,1],返回6。
形式化定义:
- 输入:一个整数数组height,其中height[i]表示第i个柱子的高度
- 输出:一个整数,表示可以接住的雨水总量
3. 暴力解法:直观但低效的实现
3.1 基本思路
最直观的解法是对于数组中的每个元素,我们找出它左边最高的柱子和右边最高的柱子,然后取两者中较小的那个,减去当前柱子的高度,就是这个位置能接的雨水量。
python复制def trap_brute_force(height):
total_water = 0
n = len(height)
for i in range(n):
max_left = max(height[:i+1])
max_right = max(height[i:])
water = min(max_left, max_right) - height[i]
total_water += water if water > 0 else 0
return total_water
3.2 复杂度分析
这种方法的时间复杂度是O(n²),因为对于每个元素,我们都要遍历整个数组来寻找左右最大值。空间复杂度是O(1),因为我们只使用了常数个额外变量。
提示:虽然这种方法简单直观,但在处理大规模数据时会非常低效,不建议在实际应用中使用。
4. 动态规划解法:空间换时间的优化
4.1 优化思路
我们可以预先计算每个位置的左右最大值,存储在两个数组中,这样就不需要在每次迭代时都重新计算。这体现了动态规划中"记忆化"的思想。
4.2 具体实现
python复制def trap_dp(height):
if not height:
return 0
n = len(height)
left_max = [0] * n
right_max = [0] * n
total_water = 0
# 从左向右扫描,记录每个位置左边的最大值
left_max[0] = height[0]
for i in range(1, n):
left_max[i] = max(left_max[i-1], height[i])
# 从右向左扫描,记录每个位置右边的最大值
right_max[-1] = height[-1]
for i in range(n-2, -1, -1):
right_max[i] = max(right_max[i+1], height[i])
# 计算每个位置能接的雨水量
for i in range(n):
total_water += min(left_max[i], right_max[i]) - height[i]
return total_water
4.3 复杂度分析
这种方法的时间复杂度降低到了O(n),因为我们只需要三次遍历数组。空间复杂度是O(n),因为我们需要两个额外的数组来存储左右最大值。
5. 双指针解法:最优的空间优化方案
5.1 核心思想
我们可以进一步优化空间复杂度,使用双指针的方法在O(1)的空间内解决问题。关键在于同时从左右两边向中间扫描,动态维护左右最大值。
5.2 实现细节
python复制def trap_two_pointers(height):
if not height:
return 0
left, right = 0, len(height) - 1
left_max = right_max = 0
total_water = 0
while left < right:
if height[left] < height[right]:
if height[left] >= left_max:
left_max = height[left]
else:
total_water += left_max - height[left]
left += 1
else:
if height[right] >= right_max:
right_max = height[right]
else:
total_water += right_max - height[right]
right -= 1
return total_water
5.3 为什么这种方法有效
这种方法之所以有效,是因为我们每次移动较矮的那一侧的指针。这意味着我们总是可以确定当前指针位置的水量是由另一侧的最大值和当前侧的最大值中的较小值决定的。
6. 单调栈解法:另一种视角的解决方案
6.1 栈的应用思路
使用单调递减栈可以跟踪可能形成"凹槽"的柱子。当遇到比栈顶高的柱子时,说明可能形成了一个可以接雨水的区域。
6.2 具体实现
python复制def trap_stack(height):
stack = []
total_water = 0
current = 0
while current < len(height):
while stack and height[current] > height[stack[-1]]:
top = stack.pop()
if not stack:
break
distance = current - stack[-1] - 1
bounded_height = min(height[current], height[stack[-1]]) - height[top]
total_water += distance * bounded_height
stack.append(current)
current += 1
return total_water
6.3 适用场景分析
这种方法特别适合处理某些特定模式的高度图,虽然时间复杂度仍然是O(n),但在某些情况下可能比其他方法更直观。
7. 性能对比与实测分析
7.1 不同解法的性能测试
为了比较这些方法的实际性能,我使用了一个包含10000个随机高度的大数组进行测试:
- 暴力解法:约4.5秒
- 动态规划:约0.02秒
- 双指针:约0.01秒
- 单调栈:约0.03秒
7.2 内存使用情况
- 暴力解法:O(1)
- 动态规划:O(n)
- 双指针:O(1)
- 单调栈:O(n)(最坏情况)
7.3 选择建议
在实际应用中,双指针方法通常是首选,因为它兼具了时间效率和空间效率。但在某些特殊情况下,其他方法可能更有优势。
8. 边界条件与异常处理
8.1 常见边界情况
- 空数组:应该返回0
- 单调递增或递减的高度:无法接住雨水
- 所有高度相同:无法接住雨水
- 只有一个或两个柱子:无法形成凹槽接住雨水
8.2 防御性编程建议
在实现时,应该首先检查这些边界条件,避免不必要的计算。例如:
python复制if not height or len(height) < 3:
return 0
9. 实际应用与变种问题
9.1 实际应用场景
这个问题不仅仅是算法题,在实际中有很多应用:
- 城市规划中的雨水收集系统设计
- 地理信息系统中的地形分析
- 图像处理中的某些算法
9.2 常见变种问题
- 二维接雨水问题(更复杂的版本)
- 考虑柱子宽度不为1的情况
- 考虑蒸发或渗透等因素的动态版本
10. 个人经验与优化技巧
在实际编码和面试中,我有以下几点经验分享:
- 先从暴力解法开始,确保理解问题本质,再逐步优化
- 画图辅助理解,特别是对于双指针方法
- 注意边界条件的处理,这是面试中常见的考察点
- 对于Python实现,注意列表切片操作(如height[:i])会创建新列表,影响性能
- 在解释解法时,可以从水的特性出发:水会流向最低处,被两侧较高的边界限制
一个常见的优化技巧是预先处理边界条件,避免进入主循环:
python复制def trap_optimized(height):
if not height:
return 0
# 找到全局最高点
max_idx = height.index(max(height))
total = 0
# 处理左半部分
left_max = 0
for h in height[:max_idx]:
if h > left_max:
left_max = h
else:
total += left_max - h
# 处理右半部分
right_max = 0
for h in reversed(height[max_idx+1:]):
if h > right_max:
right_max = h
else:
total += right_max - h
return total
这种方法结合了双指针和动态规划的思想,在某些情况下可能更高效。
