1. 问题概述与核心难点
接雨水问题是力扣(LeetCode)上经典的算法题目之一,编号42。题目描述如下:给定n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
这个问题的直观理解就是计算这些柱子之间的"凹槽"能够存储多少雨水。看似简单,但实际解决时需要处理多种边界情况和复杂地形。我在第一次遇到这个问题时,尝试了多种解法,从最直观的暴力解法到逐步优化的动态规划、双指针等方法,每种思路都有其独特的思考角度和实现技巧。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力解法解析
2.1 按列计算法
最直观的暴力解法是按列计算每个柱子能接的雨水量。对于数组中的每个元素,我们分别向左和向右扫描,找到左右两侧的最高柱子,然后取两者中的较小值,减去当前柱子的高度,就是这个柱子能接的雨水量。
python复制def trap_brute_force(height):
total = 0
n = len(height)
for i in range(1, n-1):
max_left = max(height[:i])
max_right = max(height[i+1:])
water = min(max_left, max_right) - height[i]
if water > 0:
total += water
return total
这个解法的时间复杂度是O(n²),因为对于每个元素,我们都要遍历整个数组来寻找左右最大值。空间复杂度是O(1),因为没有使用额外的存储空间。
注意:在实际编码时,边界条件(第一个和最后一个柱子)不需要处理,因为它们不可能形成凹槽接住雨水。
2.2 按行计算法
另一种暴力解法是按行计算,想象水从顶部一层层填满。对于每一层,我们找到最左边和最右边的柱子,计算它们之间的"凹槽"能接多少水。
python复制def trap_layer_by_layer(height):
if not height:
return 0
max_height = max(height)
total = 0
for layer in range(1, max_height+1):
left = 0
while left < len(height) and height[left] < layer:
left += 1
right = len(height) - 1
while right >= 0 and height[right] < layer:
right -= 1
for i in range(left, right):
if height[i] < layer:
total += 1
return total
这种方法的时间复杂度是O(m×n),其中m是最高柱子的高度。在最坏情况下(柱子非常高),这个解法效率会非常低。
3. 优化解法:动态规划
3.1 动态规划思路
暴力解法的主要问题在于重复计算。对于每个柱子,我们都要扫描整个数组来寻找左右最大值。动态规划的思路是预先计算并存储这些最大值,避免重复计算。
我们可以创建两个数组:
- left_max[i]:表示第i个柱子左边(包括自己)的最高柱子
- right_max[i]:表示第i个柱子右边(包括自己)的最高柱子
然后,对于每个柱子i,能接的雨水量就是min(left_max[i], right_max[i]) - height[i]
python复制def trap_dp(height):
if not height:
return 0
n = len(height)
left_max = [0] * n
right_max = [0] * n
total = 0
# 从左向右计算left_max
left_max[0] = height[0]
for i in range(1, n):
left_max[i] = max(height[i], left_max[i-1])
# 从右向左计算right_max
right_max[-1] = height[-1]
for i in range(n-2, -1, -1):
right_max[i] = max(height[i], right_max[i+1])
# 计算总雨水量
for i in range(1, n-1):
total += min(left_max[i], right_max[i]) - height[i]
return total
这个解法的时间复杂度是O(n),因为我们只需要三次遍历数组。空间复杂度也是O(n),因为需要存储两个额外的数组。
3.2 动态规划优化
我们可以进一步优化空间复杂度。观察发现,在计算right_max时,我们其实可以同时计算雨水量,而不需要存储整个right_max数组。
python复制def trap_dp_optimized(height):
if not height:
return 0
n = len(height)
left_max = [0] * n
total = 0
left_max[0] = height[0]
for i in range(1, n):
left_max[i] = max(height[i], left_max[i-1])
right_max = height[-1]
for i in range(n-2, 0, -1):
right_max = max(height[i], right_max)
total += min(left_max[i], right_max) - height[i]
return total
这个优化版本的空间复杂度仍然是O(n),因为left_max数组还是需要的。不过在实际应用中,这种优化可以减少一些内存使用。
4. 最优解法:双指针法
4.1 双指针思路
动态规划虽然优化了时间复杂度,但空间复杂度仍然是O(n)。我们可以使用双指针法将空间复杂度降低到O(1)。
基本思路是:使用两个指针left和right分别从数组的两端向中间移动。同时维护left_max和right_max变量,表示左右两边已经遍历过的最高柱子。
关键点在于:对于left指针指向的柱子,如果left_max < right_max,那么left柱子能接的雨水量就由left_max决定,反之亦然。
python复制def trap_two_pointers(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),只使用了常数个额外变量。这是最优的解法。
4.2 双指针法的理解难点
双指针法的关键在于理解为什么可以这样移动指针。核心观察点是:对于left指针指向的柱子,如果left_max < right_max,那么left柱子能接的雨水量就由left_max决定,因为右边至少有一个right_max比left_max大,可以"挡住"水。
同理,如果left_max > right_max,那么right柱子能接的雨水量就由right_max决定。这种理解方式可以帮助我们更好地掌握双指针法的精髓。
5. 其他解法与变种
5.1 单调栈解法
除了上述方法,还可以使用单调栈来解决这个问题。单调栈解法更适合处理类似"下一个更大元素"这类问题,但在接雨水问题上也有其独特优势。
python复制def trap_stack(height):
stack = []
total = 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 += distance * bounded_height
stack.append(current)
current += 1
return total
单调栈解法的时间复杂度也是O(n),每个元素最多被压入和弹出栈一次。空间复杂度在最坏情况下是O(n)。
5.2 二维接雨水问题
接雨水问题还有二维的变种,即计算二维地形能接多少雨水。这类问题通常需要使用优先队列(堆)或者更复杂的算法来解决,是力扣上的困难题目。
6. 实际应用与面试技巧
接雨水问题虽然看起来是一个抽象的算法题,但实际上有很多实际应用场景,比如:
- 城市排水系统设计
- 地形蓄水能力计算
- 图像处理中的某些算法
- 资源分配问题
在面试中遇到这个问题时,建议按照以下步骤进行:
- 先明确问题,确保理解正确
- 提出暴力解法并分析复杂度
- 思考优化方向(通常是减少重复计算)
- 提出动态规划解法
- 进一步优化空间复杂度(双指针法)
- 讨论边界条件和特殊情况
在编码时要注意:
- 处理空数组的情况
- 确保不处理第一个和最后一个柱子
- 注意数组越界问题
- 确保计算结果非负
我在实际面试中多次遇到这个问题及其变种,发现面试官不仅关注最终解法,更看重思考过程和优化思路。因此,即使知道最优解法,也应该从暴力解法开始,逐步优化,展示完整的思考链条。
