1. 算法训练营中的经典难题解析
作为一名经历过多次算法面试的老兵,我深知"接雨水"和"柱状图中最大的矩形"这两道题在技术面试中的分量。这两道LeetCode hard级别的题目,不仅考察对数据结构的理解,更是对问题建模能力和思维缜密度的绝佳测试。今天我就带大家深入剖析这两道经典题目,分享我在代码随想录算法训练营中总结的解题心法。
记得第一次遇到"接雨水"问题时,我花了整整三小时才勉强通过所有测试用例。而"柱状图中最大的矩形"更是在面试中让我栽过跟头。经过系统性的训练和反复实践,我发现这两道题虽然表面差异很大,但核心都离不开对单调栈这一数据结构的灵活运用。下面我们就从最基础的暴力解法开始,逐步优化到最优解。
2. 42. 接雨水问题深度剖析
2.1 问题描述与直观理解
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。这是LeetCode上经典的"接雨水"问题描述。
想象一下城市的天际线,高矮不一的建筑物排列在一起。下雨时,雨水会在建筑物之间的凹陷处积聚。我们的任务就是计算这些"凹陷"能存储多少雨水。关键在于理解:对于每个位置i,它能存储的水量取决于它左边最高和右边最高柱子中的较小值。
提示:很多初学者会误以为只需要看相邻柱子的高度差,实际上必须考虑全局的左右边界。
2.2 暴力解法及其局限性
最直观的解法是对于每个位置i:
- 向左扫描找到最大高度left_max
- 向右扫描找到最大高度right_max
- 当前位置i的储水量为min(left_max, right_max) - height[i]
python复制def trap_brute_force(height):
total = 0
n = len(height)
for i in range(1, n-1):
left_max = max(height[:i])
right_max = max(height[i+1:])
water = min(left_max, right_max) - height[i]
if water > 0:
total += water
return total
这种方法时间复杂度O(n²),空间复杂度O(1)。在LeetCode上会超时,但作为理解问题的基础很有价值。
2.3 动态规划优化解法
观察到暴力解法中有大量重复计算(每个位置都反复计算左右最大值),我们可以用动态规划预处理:
- 从左到右扫描,记录每个位置左侧最大值left_max[i]
- 从右到左扫描,记录每个位置右侧最大值right_max[i]
- 最后遍历计算每个位置的储水量
python复制def trap_dp(height):
if not height:
return 0
n = len(height)
left_max = [0] * n
right_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[-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
这种方法将时间复杂度优化到O(n),空间复杂度O(n)。是面试中期望的基本解法。
2.4 双指针的极致优化
进一步观察可以发现,我们其实不需要存储所有的left_max和right_max。使用双指针可以在O(1)空间内解决问题:
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
这个解法的精妙之处在于:我们只需要知道当前指针所指位置的较小边界的最大值,而不需要知道另一侧的具体情况。
2.5 单调栈解法及其适用场景
虽然双指针解法已经很优秀,但单调栈提供了另一种思路,特别适合处理"下一个更大元素"这类问题:
python复制def trap_monotonic_stack(height):
stack = []
total = 0
for i in range(len(height)):
while stack and height[i] > height[stack[-1]]:
top = stack.pop()
if not stack:
break
distance = i - stack[-1] - 1
bounded_height = min(height[i], height[stack[-1]]) - height[top]
total += distance * bounded_height
stack.append(i)
return total
单调栈解法的时间复杂度O(n),空间复杂度O(n)。虽然不如双指针高效,但思路非常值得学习,为下一题的解法奠定了基础。
3. 84. 柱状图中最大的矩形问题解析
3.1 问题描述与直观理解
给定 n 个非负整数,表示柱状图中各个柱子的高度。每个柱子彼此相邻且宽度为1。求在该柱状图中,能够勾勒出的最大矩形的面积。
与接雨水问题不同,这里我们需要找到的是由柱子组成的最大矩形区域。关键在于理解:对于每个柱子i,它能构成的最大矩形宽度取决于左右第一个比它矮的柱子的位置。
3.2 暴力解法的局限性
最直观的解法是对于每个柱子i:
- 向左找到第一个比height[i]小的柱子left
- 向右找到第一个比height[i]小的柱子right
- 计算面积height[i] * (right - left - 1)
python复制def largestRectangleArea_brute(height):
max_area = 0
n = len(height)
for i in range(n):
left = i
while left >= 0 and height[left] >= height[i]:
left -= 1
right = i
while right < n and height[right] >= height[i]:
right += 1
max_area = max(max_area, height[i] * (right - left - 1))
return max_area
这种方法时间复杂度O(n²),在LeetCode上会超时,但有助于理解问题本质。
3.3 单调栈的精妙应用
单调栈是解决此类"边界查找"问题的利器。我们维护一个栈,保持栈内元素对应的高度单调递增:
python复制def largestRectangleArea_monotonic_stack(height):
height.append(0) # 添加哨兵
stack = [-1] # 初始化栈
max_area = 0
for i in range(len(height)):
while stack and height[i] < height[stack[-1]]:
h = height[stack.pop()]
w = i - stack[-1] - 1
max_area = max(max_area, h * w)
stack.append(i)
height.pop() # 恢复原数组
return max_area
这个解法的关键在于:
- 添加哨兵节点简化边界处理
- 当遇到较小元素时,计算栈顶元素能构成的最大矩形
- 时间复杂度O(n),空间复杂度O(n)
3.4 分治解法及其优化
虽然单调栈是最优解,但分治思路也值得了解:
python复制def largestRectangleArea_divide_conquer(height):
def helper(start, end):
if start > end:
return 0
min_index = start
for i in range(start, end+1):
if height[i] < height[min_index]:
min_index = i
return max(height[min_index] * (end - start + 1),
helper(start, min_index-1),
helper(min_index+1, end))
return helper(0, len(height)-1)
分治解法在最坏情况下会退化到O(n²),但可以通过线段树优化到O(nlogn)。
4. 两道题目的对比与总结
4.1 解题思路的异同点
虽然两道题目都涉及柱状图,但解题思路有本质区别:
- 接雨水:关注的是"凹陷"部分,计算每个位置上方能存储的水量
- 最大矩形:关注的是"凸起"部分,寻找最大的连续矩形区域
有趣的是,单调栈在两题中都有应用,但使用方式不同:
- 接雨水:维护递减栈,遇到较高元素时计算储水
- 最大矩形:维护递增栈,遇到较低元素时计算面积
4.2 面试中的常见变体
在面试中,这两道题常有各种变体:
-
接雨水变体:
- 3D接雨水问题
- 考虑柱子宽度不为1的情况
- 允许雨水从侧面流出
-
最大矩形变体:
- 矩阵中的最大矩形(LeetCode 85)
- 圆形柱子的最大矩形
- 考虑柱子有负高度的情况
4.3 个人实战经验分享
在多次面试和刷题过程中,我总结了以下几点经验:
- 对于接雨水问题,双指针解法虽然高效,但单调栈的思路更容易扩展到其他类似问题
- 最大矩形问题中,添加哨兵节点可以大大简化代码逻辑
- 实际面试中,面试官往往更关注解题思路的演进过程,而非直接写出最优解
- 这两道题的变体经常出现在各大公司的面试中,建议彻底理解后尝试解决相关变体
注意:在实现单调栈解法时,边界条件的处理是最容易出错的地方。建议先在纸上画出几个测试案例,手动模拟栈的变化过程。
经过代码随想录算法训练营的系统训练,我对这两道题的理解从最初的懵懂到现在的清晰,最大的收获不是记住了解法,而是掌握了分析问题和优化解法的系统性思维。这种能力对于解决其他算法问题同样适用。
