LeetCode 407 接雨水 II(3D 版):python3 题解
题目链接:407. 接雨水 II
目录
1. 题目理解
问题描述:
给定一个 \(m \times n\) 的二维矩阵 heightMap,每个单元格代表一个地形的高度。假设下雨后水能填充在地形的低洼处,请计算这个三维地形最多能接住多少体积的雨水。
核心规则:
- 水可以向上下左右四个方向流动。
- 水会从矩阵的边界流出。这意味着边界上的单元格无法接住雨水(除非边界本身围成了一个更高的圈,但在本题定义中,矩阵最外圈视为与外界连通,水会流走)。
- 一个内部单元格能接多少水,取决于它周围“围墙”的最低高度。这就像木桶效应:一个木桶能装多少水,取决于最短的那块木板。在二维矩阵中,这个“最短木板”是指从该单元格出发到达边界的所有路径中,路径上最大高度的最小值。
示例分析:
输入:heightMap = [
[1,4,3,1,3,2],
[3,2,1,3,2,4],
[2,3,3,2,3,1]
]
想象这是一个微缩的地形模型。最外圈是边界,水会从这里流走。内部低洼的地方(比如高度为 1 或 2 的地方)如果被周围高度为 3 或 4 的地方包围,水就会积存下来。积存的高度上限是包围它的最低墙壁高度。
2. 解题思路与核心思想
2.1 为什么不能用一维接雨水的思路?
在一维接雨水(LeetCode 42)中,我们只需要关注左边最高和右边最高。但在二维中,水有四个流动方向。如果我们简单地用 DFS 或 BFS 从内向外或从外向内遍历,无法保证处理顺序的正确性。
- 错误思路:如果先处理了一个高度为 10 的边界,认为内部水位限制是 10,但后来发现旁边还有一个高度为 3 的边界,水其实会从 3 的地方流走。
- 正确思路:水总是从最低的缺口流出。因此,我们应该优先处理高度最低的边界单元格,逐步向内收缩。
2.2 优先队列(最小堆)+ 广度优先搜索 (BFS)【⭐】
这是本题的最优解法,类似于 Dijkstra 算法求最短路径的思想。
算法流程:
- 初始化边界:将矩阵最外圈的所有单元格放入一个最小堆(Priority Queue)中。堆中存储
(高度,行坐标,列坐标)。同时,将这些单元格标记为visited(已访问),因为水是从这里开始“渗入”或“流出”的起点。 - 维护当前水位线:从堆中弹出高度最小的单元格
(h, r, c)。这个h代表了当前包围内部区域的“最低围墙高度”。 - 探索邻居:检查该单元格的上下左右四个邻居。
- 如果邻居未访问过:
- 情况 A(邻居更低):如果邻居的高度
neighbor_h < h,说明水可以积存在这里。积水量为h - neighbor_h。对于更内部的单元格来说,这个位置被水填满后,其有效高度变成了h(因为水面是平的)。 - 情况 B(邻居更高):如果邻居的高度
neighbor_h >= h,说明这里不能积水,它成为了新的、更高的围墙。对于更内部的单元格,其有效高度限制变成了neighbor_h。 - 入堆:无论哪种情况,都将邻居放入堆中,其入堆高度为
max(h, neighbor_h)(即有效高度),并标记为已访问。
- 情况 A(邻居更低):如果邻居的高度
- 如果邻居未访问过:
- 重复:重复步骤 2-3,直到堆为空。所有内部单元格都被访问过后,累加的积水量即为答案。
3. 代码实现 (Python 3)
from typing import List
import heapq
class Solution:
def trapRainWater(self, heightMap: List[List[int]]) -> int:
# 获取矩阵的行数和列数
if not heightMap or not heightMap[0]:
return 0
m, n = len(heightMap), len(heightMap[0])
# 如果行数或列数小于 3,中间没有空间可以接水,直接返回 0
# 因为接水至少需要周围有一圈包围,3x3 是最小的可能接水矩阵(中间 1 个格子)
if m < 3 or n < 3:
return 0
# visited 矩阵用于记录哪些单元格已经处理过(加入过堆)
visited = [[False] * n for _ in range(m)]
# 最小堆,存储元组 (高度,行,列)
# Python 的 heapq 默认是最小堆,符合我们需要优先处理最低边界的需求
heap = []
# 1. 初始化:将所有边界单元格加入堆,并标记为已访问
for i in range(m):
for j in range(n):
# 判断是否在边界上:第一行、最后一行、第一列、最后一列
if i == 0 or i == m - 1 or j == 0 or j == n - 1:
heapq.heappush(heap, (heightMap[i][j], i, j))
visited[i][j] = True
# 记录总接水量
total_water = 0
# 定义四个方向:上、下、左、右
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# 2. 开始 BFS 遍历,每次取出当前边界高度最低的点
while heap:
# 弹出高度最小的单元格
height, r, c = heapq.heappop(heap)
# 遍历该单元格的四个邻居
for dr, dc in directions:
nr, nc = r + dr, c + dc
# 检查邻居是否在矩阵范围内 且 未被访问过
if 0 <= nr < m and 0 <= nc < n and not visited[nr][nc]:
# 标记邻居为已访问,防止重复处理
visited[nr][nc] = True
# 核心逻辑:
# 如果邻居的高度小于当前弹出的边界高度 height,
# 说明水会被当前的边界挡住,积存在邻居位置。
# 积水量 = 边界高度 - 邻居地面高度
if heightMap[nr][nc] < height:
total_water += height - heightMap[nr][nc]
# 对于更内部的单元格来说,这个位置被水填满后,有效高度变成了 height
# 所以入堆时高度记为 height
heapq.heappush(heap, (height, nr, nc))
else:
# 如果邻居高度大于等于当前边界高度,说明这里不会积水,
# 它成为了新的更高的边界。
# 入堆时高度记为其实际高度
heapq.heappush(heap, (heightMap[nr][nc], nr, nc))
return total_water
4. 复杂度分析
- 时间复杂度: \(O(m \times n \times \log(m \times n))\)
- 矩阵中共有 \(m \times n\) 个单元格,每个单元格最多进入堆一次,弹出一次。
- 堆的操作(push/pop)时间复杂度为 \(O(\log K)\),其中 \(K\) 是堆中元素数量,最大为 \(m \times n\)。
- 因此总时间复杂度为 \(O(mn \log(mn))\)。
- 空间复杂度: \(O(m \times n)\)
- 需要一个
visited矩阵来记录访问状态。 - 最坏情况下,堆中可能存储大部分单元格。
- 需要一个
5. 其他解法思路讨论
除了上述的 优先队列 + BFS(最推荐)之外,还有以下思路可供参考,但在本题中效率或实现难度不如前者:
5.1 二分答案 + BFS/DFS 验证
- 思路:我们可以二分搜索最终的水位高度 \(H\)。
- 假设水位高度为 \(H\),我们检查所有高度小于 \(H\) 的单元格。
- 使用 BFS/DFS 检查这些低洼区域是否与矩阵边界连通。
- 如果连通,说明水会流走,该区域在高度 \(H\) 下无法存水。
- 如果不连通,说明被高墙包围,可以存水。
- 缺点:需要多次遍历矩阵,复杂度约为 \(O(mn \log(\max(\text{height})))\)。虽然也是多项式时间,但常数较大,且实现起来比堆方法复杂。
5.2 并查集 (Union-Find)
- 思路:类似于 Kruskal 算法。
- 将所有单元格按高度从小到大排序。
- 从最低的单元格开始,将其与周围高度相近的单元格合并集合。
- 维护每个集合是否“接触边界”。
- 如果一个集合不接触边界,且当前处理的高度高于集合内单元格高度,则产生积水。
- 缺点:实现逻辑较为繁琐,需要处理集合合并和边界标记,空间开销也较大。
5.3 为什么普通 BFS/DFS 不行?
- 如果从任意点开始 BFS,无法确定当前的“水位限制”。
- 如果从边界开始普通 BFS(队列而非优先队列),可能会先处理高度为 10 的边界点,再处理高度为 3 的边界点。当处理 10 的时候,可能会错误地认为相邻的内部低点可以积水到 10,但实际上水会从 3 的地方流走。必须保证每次处理的都是当前所有边界中最低的那个点,这就是必须使用最小堆的原因。
6. 总结
本题是经典的“木桶效应”在二维网格上的应用。
- 关键点:水从最低处流出,所以必须从边界的最低点开始向内处理。
- 数据结构:最小堆(Priority Queue)是维护“当前最低边界”的最佳工具。
- 状态更新:入堆的高度不是单元格的原始高度,而是
max(原始高度,当前边界高度),这代表了水填平后的有效高度。
通过上述代码,我们可以高效、准确地计算出二维地形图的接雨水量。

浙公网安备 33010602011771号