接雨水 II 图解题解
这道题到底在问什么
- 输入
- heightMap=[[3,3,3,3],[3,1,2,3],[3,2,1,3],[3,3,3,3]]
- 输出
- 6(四周都是 3,中间四格 1,2,2,1 全被围住,各接到 3 减自身高度的水)
最优解:一步一步想明白
- 3记住三句话:边界进小根堆、每次取最矮的边界往里看、邻居接水 = max(0, 边界高 减 自身高),然后以 max 抬高后入堆。下面逐帧套它。
- 4第一步:把整圈边界格都压进小根堆(蓝色=已入堆的边界)。这一圈就是初始的「围墙」,水要漏只能从它们里最矮的地方漏。这里边界全是 3,中间四格还空着(灰)。
- 5从堆里取出当前最矮的边界 (0,0)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 6从堆里取出当前最矮的边界 (0,3)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 7从堆里取出当前最矮的边界 (1,0)、高 3(琥珀)。看它还没访问的内邻居,水能不能从这里灌进去。
- 8往右看 (1,1)、高 1(紫),它比墙 3 矮,能接水 = 3 − 1 = 2(蓝水)。累计水量 = 2。再以有效高度 max(3, 1) = 3 把它压回堆,当新边界。
- 9从堆里取出当前最矮的边界 (1,3)、高 3(琥珀)。看它还没访问的内邻居,水能不能从这里灌进去。
- 10往左看 (1,2)、高 2(紫),它比墙 3 矮,能接水 = 3 − 2 = 1(蓝水)。累计水量 = 3。再以有效高度 max(3, 2) = 3 把它压回堆,当新边界。
- 11从堆里取出当前最矮的边界 (2,0)、高 3(琥珀)。看它还没访问的内邻居,水能不能从这里灌进去。
- 12往右看 (2,1)、高 2(紫),它比墙 3 矮,能接水 = 3 − 2 = 1(蓝水)。累计水量 = 4。再以有效高度 max(3, 2) = 3 把它压回堆,当新边界。
- 13从堆里取出当前最矮的边界 (2,3)、高 3(琥珀)。看它还没访问的内邻居,水能不能从这里灌进去。
- 14往左看 (2,2)、高 1(紫),它比墙 3 矮,能接水 = 3 − 1 = 2(蓝水)。累计水量 = 6。再以有效高度 max(3, 1) = 3 把它压回堆,当新边界。
- 15从堆里取出当前最矮的边界 (3,0)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 16从堆里取出当前最矮的边界 (3,3)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 17从堆里取出当前最矮的边界 (0,1)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 18从堆里取出当前最矮的边界 (3,1)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 19从堆里取出当前最矮的边界 (0,2)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 20从堆里取出当前最矮的边界 (3,2)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 21从堆里取出当前最矮的边界 (1,1)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 22从堆里取出当前最矮的边界 (1,2)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 23从堆里取出当前最矮的边界 (2,1)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 24从堆里取出当前最矮的边界 (2,2)、高 3(琥珀)。它四周的格子要么是边界、要么早被访问过,没有新邻居可灌,直接丢弃,继续取下一个。
- 25堆空了,所有格子都被访问过(中间四个凹格都蓄上了蓝水)。把每格接到的水加起来:2 + 1 + 1 + 2 = 6。这就是整张图能接的雨水总量。
⚠️ 容易写错的地方
✗ 错:直接套一维「接雨水」的左右双指针
✓ 对:用最小堆从最矮边界向内灌
二维里水会往四面八方漏,没有单一的「左墙/右墙」,必须按全局最矮边界推进
✗ 错:入堆时用格子原始高度当新边界
✓ 对:入堆用有效高度 max(墙高, 自身高)
一个矮格被灌水后,它的「水面」其实被抬到了墙高,对更里面的格子来说有效高度是 max,不是原值
✗ 错:从某个角或某条边开始单向 BFS
✓ 对:一开始把整圈边界全部入堆
最矮的漏水口可能在任意一条边上,只有全边界入堆,堆顶才始终是全局最矮边界
完整代码(Python / C++ / Java)
Python
from typing import List
import heapq
class Solution:
def trapRainWater(self, heightMap: List[List[int]]) -> int:
m, n = len(heightMap), len(heightMap[0])
if m < 3 or n < 3:
return 0
seen = [[False] * n for _ in range(m)]
heap = []
for r in range(m):
for c in (0, n - 1):
heapq.heappush(heap, (heightMap[r][c], r, c)); seen[r][c] = True
for c in range(1, n - 1):
for r in (0, m - 1):
heapq.heappush(heap, (heightMap[r][c], r, c)); seen[r][c] = True
ans = 0
while heap:
h, r, c = heapq.heappop(heap)
for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
nr, nc = r + dr, c + dc
if 0 <= nr < m and 0 <= nc < n and not seen[nr][nc]:
seen[nr][nc] = True
ans += max(0, h - heightMap[nr][nc])
heapq.heappush(heap, (max(h, heightMap[nr][nc]), nr, nc))
return ansC++
#include <queue>
#include <vector>
using namespace std;
class Solution {
public:
int trapRainWater(vector<vector<int>>& heightMap) {
int m = heightMap.size(), n = heightMap[0].size();
if (m < 3 || n < 3) return 0;
using T = tuple<int,int,int>;
priority_queue<T, vector<T>, greater<T>> pq;
vector<vector<int>> seen(m, vector<int>(n));
for (int r = 0; r < m; ++r) for (int c : {0, n - 1}) { pq.push({heightMap[r][c], r, c}); seen[r][c] = 1; }
for (int c = 1; c < n - 1; ++c) for (int r : {0, m - 1}) { pq.push({heightMap[r][c], r, c}); seen[r][c] = 1; }
int ans = 0, dirs[5] = {1,0,-1,0,1};
while (!pq.empty()) {
auto [h, r, c] = pq.top(); pq.pop();
for (int d = 0; d < 4; ++d) {
int nr = r + dirs[d], nc = c + dirs[d+1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && !seen[nr][nc]) {
seen[nr][nc] = 1;
ans += max(0, h - heightMap[nr][nc]);
pq.push({max(h, heightMap[nr][nc]), nr, nc});
}
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int trapRainWater(int[][] heightMap) {
int m = heightMap.length, n = heightMap[0].length;
if (m < 3 || n < 3) return 0;
PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
boolean[][] seen = new boolean[m][n];
for (int r = 0; r < m; r++) for (int c : new int[]{0, n - 1}) { pq.offer(new int[]{heightMap[r][c], r, c}); seen[r][c] = true; }
for (int c = 1; c < n - 1; c++) for (int r : new int[]{0, m - 1}) { pq.offer(new int[]{heightMap[r][c], r, c}); seen[r][c] = true; }
int ans = 0;
int[] dirs = {1,0,-1,0,1};
while (!pq.isEmpty()) {
int[] cur = pq.poll();
for (int d = 0; d < 4; d++) {
int nr = cur[1] + dirs[d], nc = cur[2] + dirs[d + 1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && !seen[nr][nc]) {
seen[nr][nc] = true;
ans += Math.max(0, cur[0] - heightMap[nr][nc]);
pq.offer(new int[]{Math.max(cur[0], heightMap[nr][nc]), nr, nc});
}
}
}
return ans;
}
}复杂度
时间
O(mn·log(mn))
每个格子最多入堆、出堆一次,堆里最多 O(mn) 个元素,每次堆操作 O(log(mn)),总共 O(mn·log(mn))
空间
O(mn)
seen 标记矩阵 O(mn),堆最坏也是 O(mn) 量级
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 接雨水 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么二维接雨水不能像一维那样用双指针?+
一维里每个柱子能接的水 = min(左边最高, 右边最高) 减自身,左右各只有一个方向,双指针够用。二维里水可以从上下左右四个方向漏,「左墙、右墙」退化成了「环绕一圈的边界」,某格能蓄多高水取决于包围它的整圈边界里最矮的那处。这是一个全局的、动态变化的最矮边界,正好用最小堆维护:每次取全局最矮边界、向内推进、把新格以有效高度入堆。
这个最小堆做法和最短路里的 Dijkstra 有什么关系?+
结构几乎一样。Dijkstra 每次从堆里取「当前距离最小」的点去松弛邻居;这里每次取「当前高度最矮」的边界去灌邻居。区别只在「合成新值」的方式:Dijkstra 是路径求和、取 min 更新,这里是用 max(墙高, 自身高) 当新边界、并顺手算 max(0, 墙高 减 自身高) 的接水量。可以把它理解成在网格上跑一遍以「有效高度」为距离的最小堆遍历。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 接雨水 II 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。