不是我看不起这题,说实话,华为OD机试真题里能同时考察“搜索、优先队列、面积统计、边界判断”的题目真不多,959这套“水库溃坝填补”算是比较典型的一道。题干看起来又是水库又是溃坝,一上来就把不少人唬住了,实际上把场景翻译成算法语言,本质就是一道二维地形积水题。当年我刷到类似题目的时候也卡了一段时间,因为这题和普通“从起点往外扩散”的BFS不太一样,它涉及一个动态变化的水位概念,处理不好就会多算少算一片区域。
这篇文章我打算把这道题从读题到建模、从思路到多语言代码、从易错点到同类变体全部分析一遍,覆盖C++、Java、Python、C语言、JavaScript五种实现方式。正在准备华为OD机试的朋友可以直接拿来当模板参考,刚入坑算法的小白也能按着思路一步步推明白。先说清楚,这道题解析的重点不在“会写DFS”,而在搞明白“水面高度到底什么时候涨、怎么涨”。
1. 先读懂题:水库溃坝填补到底在算啥
1.1 从工程场景到算法模型
把题干里的“水库溃坝”翻译成数据模型,其实就三步。第一步,给你一个二维网格,每个格子有一个整数,代表该点的地面高度;第二步,给一个溃口坐标,也就是水从哪个位置灌入;第三步,水会在低洼区域积蓄,被高处挡住,最终问你整个水库能存下多少体积的水。
说人话版本就是:你拿一个不规则的盆,盆底被砸了个洞,然后往洞里倒水。水会沿着洞往四周漫,哪边低就往哪边走,遇到高过水面的“坝”就停下,被围住的区域就变成积水。这里要统计的“填补量”,就是墙和水面之间那部分空间有多大。
“水库溃坝填补”真正的考点在于:水面高度不是一个输入参数,而是要在计算过程中动态推导出来的东西。它取决于水漫过哪些低洼区域、最终被哪个方向的最高边界拦住。很多人在这一步就掉坑了,习惯性以为固定一个水位然后做BFS就行,但实际水位是会随着连通区域变化而一层层抬升的。
1.2 边界条件决定生死
做这题之前,先看题面怎么定义边界,因为边界情况直接决定两种完全不同的解法:
- 第一种设定:网格四周视为无限高的堤坝,溃口在内部,水不会流到“网格外面”。这种情况下,水只能填充内部低洼区,积水一定存在,最终答案是非零体积。
- 第二种设定:水可以从网格边缘流到外界。那就要小心了,如果溃口所在的连通区域能通向边界,水最终会全部流走,一个格子都留不住,答案直接是0。只有溃口四周被封闭高地围住时才有积水可算。
我个人的建议是写题之前先判断一下:如果溃口坐标在边界上,直接输出0。这个判断逻辑对两种设定都适用,因为即使边界是无限高墙,溃口出现在边界本来就不符合“水库内部”场景,输出0反而是安全的兜底。
还有一种更偷懒的统一做法:把原始网格外面再包一圈高度为无穷大的虚拟格子,这样不管题目怎么描述边界,水的流动都被限制在这个加了围墙的网格内部。配合优先队列BFS,这种做法能屏蔽掉边界判断带来的大量分支讨论。
1.3 数据范围与复杂度预判
华为OD机试里这类题的数据量一般不会特别夸张,网格规模通常在几十到几千之间。优先队列BFS的时间复杂度是O(nmlog(nm)),空间复杂度是O(nm)。估算一下:如果网格是1000×1000,总格子数100万,堆操作每次log级别,C++大概跑几千万次操作,稳稳的;Java也还行;Python如果写得不优化,可能会到极限,但通过率一般也不差。
所以拿到题先看一眼行列范围,再决定用哪种语言、要不要手动优化输入输出。范围一旦超过5000×5000,优先队列方案就开始吃力了,这时候得考虑更进阶的离线并查集解法,这个我在后面第5部分会补充两句。
2. 思路拆解:从暴力模拟到优先队列BFS
2.1 为什么简单的DFS模拟会翻车
新手看到“水从溃口蔓延”,第一反应肯定是DFS或者普通BFS:从起点开始,向四个方向扩散,遇到高度比当前水位低的就填充,遇到高的就停下。但稍微构造一组数据,这个方案就崩了。
假设溃口高度是10,它旁边有一个高度7的坑,这个坑又被一圈高度12的土坡围住。你从溃口灌水,水会先流到7的坑里,但水面最后能到多少?12对吧?如果只用DFS按连通性扩散,你可能在第一次遇到7的时候就把差值3填进去了,后续发现整个区域的最终水位其实是12,中间还隔了一堆别的格子呢,这时候你怎么补算后面的水位?
问题本质在于“水位”不是一个静态BFS层数,而是一个随着扩展动态变化的全局状态。普通DFS只能处理“以固定高度扩散”的场景,而这个题目里高度在不断变化。最直观的暴力方案是反复迭代:假设一个水位,做BFS看能淹多少,再提高水位再算,直到水位不再变化。但这种做法复杂度爆炸,而且边界情况极容易漏算。
2.2 关键洞察:水只会从边界最低点溢出
换个角度倒过来想。水从溃口涌出来以后,正在积水区域的“外边界”上,一定会有一个最低点,水会优先从那里溢出去。而这个最低点的高度,正是下一次水位抬升的“瓶颈”。
其实我们不需要关心水的完整流动轨迹,只需要维护两样东西:当前已经“触达”的所有格子集合,以及这个集合外边界上的最低高度。这个最低高度决定了水能不能继续往外淹,也决定了已成积水区的水面能涨到多高。
优先队列(最小堆)天然就是干这个的。堆里面存的是当前已经触达但还没最终处理的格子,堆顶永远是高度最小的那个,也就是当前最容易被水漫过的位置。每次弹出一个格子,更新水位,累加积水量,再把它的四个邻居塞进堆里,等下一次处理。这个过程思想和LeetCode 407“接雨水II”几乎一脉相承,只不过那道题从整个边界开始注水,而这题从一个内部溃口开始。
2.3 优先队列BFS的完整算法流程
写代码之前,先把流程固定下来:
- 读入n、m、地形网格、溃口坐标(sr, sc)。
- 定义一个小根堆,堆元素是三元组(高度、行、列)。
- 把溃口坐标的高度塞进堆,并标记为已访问。
- 初始化waterLevel等于溃口高度,ans等于0。
- 循环直到堆空: a. 弹出堆顶元素(curH, x, y)。 b. 如果curH大于当前水位,说明这个点比水面高,水面要涨到curH才能继续漫过它;把waterLevel更新为curH。 c. 累加积水量ans += waterLevel - curH,这就是该格子被淹没的深度。 d. 遍历上下左右四个方向,只要没访问过,就入堆并标记访问。
- 输出ans。
这个流程里最容易写错的一步是第e步和第c步的顺序:一定要先更新水位再累加积水量,否则当前格子如果是新形成的“溢出口”,它的高度就带着零;但如果它早就是积水区里的低点,它的水量就是“当前水位-该点高度”。顺序反了,答案会差出一截。
伪代码长这样:
func solve(grid, sr, sc): heap = minHeap() visited = set() heap.push((grid[sr][sc], sr, sc)) visited.add((sr, sc)) level = grid[sr][sc] ans = 0 while heap is not empty: curH, x, y = heap.pop() if curH > level: level = curH ans += level - curH for each neighbor (nx, ny): if not visited and in bounds: heap.push((grid[nx][ny], nx, ny)) visited.add((nx, ny)) return ans注意一个细节:入堆的时候就要标记visited,而不是弹出的时候再标记。因为同一个格子可能在更新水位前就被多个邻居重复加入堆中,如果不提前标记,堆里会出现同一个坐标的好几份副本,既浪费空间又会让答案计算错乱。这个小细节我见过太多人栽在上面了。
2.4 为什么这个贪心是对的
简单说明一下正确性。堆中任意时刻保存的格子代表“已经触达但还没最终确定淹没深度”的区域边缘。水如果要继续向外扩展,唯一的出口就是当前堆顶高度最低的那个格子,也就是瓶颈位置。如果堆顶高度比当前水位低,说明瓶颈已经被水漫过,淹没深度是已知的,可以结算;如果堆顶高度比当前水位高,水面必须升到堆顶高度才能继续向外走,所以水位要抬升,同时这个格子本身也参与积水计算。
因为水位只升不降,每个格子又只入堆一次,所以每个格子被处理时它的最终淹没深度已经确定,后续不会再被修改。这就是为什么这个算法能做到O(nmlog(n*m))且结果一定正确。说白了,普通BFS适合“距离决定阶段”的题,而这种“高度决定阶段”的题必须按高度排序来做。
3. 多语言实现与逐行解析
3.1 C++:优先队列 + tuple,机试最稳模板
C++在OD机试里属于最稳的选择,主要原因是STL的priority_queue太好写了,性能也高。直接套标准模板即可,代码量不大。用tuple存三元组,系统默认按第一个元素比较,刚好是我们的高度字段。
#include <bits/stdc++.h> using namespace std; int main() { int n, m; cin >> n >> m; vector<vector<int>> grid(n, vector<int>(m)); for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { cin >> grid[i][j]; } } int sr, sc; cin >> sr >> sc; // 溃口在边界,水直接流走 if (sr == 0 || sc == 0 || sr == n - 1 || sc == m - 1) { cout << 0 << endl; return 0; } // 小根堆:<高度, 行, 列> priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<tuple<int, int, int>>> pq; vector<vector<bool>> vis(n, vector<bool>(m, false)); int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; pq.emplace(grid[sr][sc], sr, sc); vis[sr][sc] = true; long long level = grid[sr][sc]; long long ans = 0; while (!pq.empty()) { auto [curH, x, y] = pq.top(); pq.pop(); if (curH > level) { level = curH; } ans += level - curH; for (auto &d : dirs) { int nx = x + d[0]; int ny = y + d[1]; if (nx < 0 || ny < 0 || nx >= n || ny >= m || vis[nx][ny]) { continue; } vis[nx][ny] = true; pq.emplace(grid[nx][ny], nx, ny); } } cout << ans << endl; return 0; }说几个C++特有的注意事项。
第一,答案要用long long。地形高度可能很大,网格也大,累积水量很容易超过int上限,尤其是CDG里的数据阴起来能出到1e9级别。第二,tuple的比较默认按字典序,正好适合这道题,但如果要存更多维度,建议手写struct并定义operator()。第三,入堆时用emplace而不是push,能减少一次拷贝构造,性能稍微好一点。
3.2 Java:Lambda比较器 + int[]数组
Java没有Python那样的tuple,也没有C++的pair,最直接的方式是往PriorityQueue里塞int数组,Java的PriorityQueue默认是小根堆,用lambda指定比较第三个元素即可。
import java.util.*; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int m = sc.nextInt(); int[][] grid = new int[n][m]; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { grid[i][j] = sc.nextInt(); } } int sr = sc.nextInt(); int sc2 = sc.nextInt(); if (sr == 0 || sc2 == 0 || sr == n - 1 || sc2 == m - 1) { System.out.println(0); return; } // 小根堆,按照数组下标2(高度)排序 PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[2], b[2])); boolean[][] vis = new boolean[n][m]; int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; pq.offer(new int[]{sr, sc2, grid[sr][sc2]}); vis[sr][sc2] = true; long level = grid[sr][sc2]; long ans = 0; while (!pq.isEmpty()) { int[] cur = pq.poll(); int x = cur[0], y = cur[1], h = cur[2]; if (h > level) { level = h; } ans += level - h; for (int[] d : dirs) { int nx = x + d[0], ny = y + d[1]; if (nx < 0 || ny < 0 || nx >= n || ny >= m || vis[nx][ny]) { continue; } vis[nx][ny] = true; pq.offer(new int[]{nx, ny, grid[nx][ny]}); } } System.out.println(ans); } }Java的PriorityQueue由于每个元素是int[],每个节点会有对象头、数组头、引用等额外开销,内存会比C++大不少。数据量到百万级别时,建议统计一下堆里的节点数量,如果内存紧张,可以自定义一个最小堆结构,只维护三个平行数组heapX、heapY、heapH,这样能大幅降低包装开销。
另外Java的Lambda比较器如果写法不对会报比较异常,建议用Integer.compare这样不会溢出。这里有个细节:不能用(a, b) -> a[2] - b[2],因为如果高度值很大,减法会有溢出风险,虽然很少触发,但机试环境里一旦触发就是莫名其妙的错误。
3.3 Python:heapq实现与性能优化
Python写这类题最大的优势是代码简洁,最大的劣势是性能。好在OD机试的数据范围一般不会让Python完全跑不动,只要别做太多无谓操作就行。
import heapq import sys def solve(): data = sys.stdin.read().strip().split() if not data: return n, m = int(data[0]), int(data[1]) idx = 2 grid = [] for _ in range(n): row = [] for _ in range(m): row.append(int(data[idx])) idx += 1 grid.append(row) sr, sc = int(data[idx]), int(data[idx + 1]) if sr == 0 or sc == 0 or sr == n - 1 or sc == m - 1: print(0) return heap = [(grid[sr][sc], sr, sc)] visited = [[False] * m for _ in range(n)] visited[sr][sc] = True level = grid[sr][sc] ans = 0 dirs = [(1, 0), (-1, 0), (0, 1), (0, -1)] while heap: cur_h, x, y = heapq.heappop(heap) if cur_h > level: level = cur_h ans += level - cur_h for dx, dy in dirs: nx, ny = x + dx, y + dy if 0 <= nx < n and 0 <= ny < m and not visited[nx][ny]: visited[nx][ny] = True heapq.heappush(heap, (grid[nx][ny], nx, ny)) print(ans) if __name__ == "__main__": solve()注意这里我用了一次性读入整个输入再split,而不是循环调用input(),因为百行千行的矩阵一个个读太慢了。Python的heapq直接用元组比较,元素顺序写成(高度,行,列)就行,高度相同时自动按行、列排序,正好。
Python还有一个隐藏性能点:visited可以用二维list,但如果你能接受坐标编码,也可以压成一维数组,比如visit = [False] * (n * m),用x * m + y作为下标,能省掉二维数组的行列索引运算。实测在10万格子的规模下,一维visit比二维list要快10%上下。
3.4 C语言:手写最小堆
C语言没有现成的堆,需要自己实现一个小根堆。机试中C语言选手的代码量确实吃亏,但一旦手写堆,整个堆排序逻辑都在掌控中,反而在极端数据下更稳。我提供一个精简但完整的实现,结构体包含高度、坐标,以及一个存堆的数组。
#include <stdio.h> #include <stdlib.h> #define MAXN 1000005 typedef struct { int h, x, y; } Node; Node heap[MAXN]; int heapSize = 0; int n, m; int grid[1005][1005]; int visited[1005][1005]; int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; void swap(Node *a, Node *b) { Node tmp = *a; *a = *b; *b = tmp; } void push(Node v) { heap[++heapSize] = v; int i = heapSize; while (i > 1 && heap[i].h < heap[i / 2].h) { swap(&heap[i], &heap[i / 2]); i /= 2; } } Node pop() { Node top = heap[1]; heap[1] = heap[heapSize--]; int i = 1; while (1) { int smallest = i; int l = i * 2; int r = i * 2 + 1; if (l <= heapSize && heap[l].h < heap[smallest].h) { smallest = l; } if (r <= heapSize && heap[r].h < heap[smallest].h) { smallest = r; } if (smallest == i) { break; } swap(&heap[i], &heap[smallest]); i = smallest; } return top; } int main() { scanf("%d %d", &n, &m); for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { scanf("%d", &grid[i][j]); } } int sr, sc; scanf("%d %d", &sr, &sc); if (sr == 0 || sc == 0 || sr == n - 1 || sc == m - 1) { printf("0\n"); return 0; } push((Node){grid[sr][sc], sr, sc}); visited[sr][sc] = 1; long long level = grid[sr][sc]; long long ans = 0; while (heapSize > 0) { Node cur = pop(); if (cur.h > level) { level = cur.h; } ans += level - cur.h; for (int d = 0; d < 4; d++) { int nx = cur.x + dirs[d][0]; int ny = cur.y + dirs[d][1]; if (nx < 0 || ny < 0 || nx >= n || ny >= m || visited[nx][ny]) { continue; } visited[nx][ny] = 1; push((Node){grid[nx][ny], nx, ny}); } } printf("%lld\n", ans); return 0; }C语言手写堆有几个关键点必须小心。第一是push之后要上滤,pop之后要下滤,顺序搞错堆序就坏了。第二是MAXN要开到至少nm的量级,实际使用中堆里最多同时存在整个网格的节点数,所以MAXN = nm + 5就够,但别开太小。第三是long long输出要用%lld,写%d会丢高位,这道题答案一爆炸就是全错,看起来又像逻辑错了又像输出错了,非常折磨人。
3.5 JavaScript/Node.js:readline读入 + 手写小根堆
华为OD机试的JavaScript环境用的是Node.js,输入输出全靠readline.readLine。代码写起来比前面几种语言繁琐,尤其是没有内置堆,得自己实现。我写一个尽量精简的小根堆版本,重点展示堆和读入流程。
const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); let lineIdx = 0; let lines = []; rl.on('line', (line) => { lines.push(line.trim()); }).on('close', () => { let ptr = 0; const first = lines[ptr++].split(' ').map(Number); const n = first[0], m = first[1]; const grid = []; for (let i = 0; i < n; i++) { grid.push(lines[ptr++].split(' ').map(Number)); } const [sr, sc] = lines[ptr++].split(' ').map(Number); if (sr === 0 || sc === 0 || sr === n - 1 || sc === m - 1) { console.log(0); return; } const visited = Array.from({ length: n }, () => Array(m).fill(false)); const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; // 最小堆 const heap = []; function push(h, x, y) { heap.push({ h, x, y }); let i = heap.length - 1; while (i > 0) { const p = Math.floor((i - 1) / 2); if (heap[i].h < heap[p].h) { [heap[i], heap[p]] = [heap[p], heap[i]]; i = p; } else { break; } } } function pop() { const top = heap[0]; const last = heap.pop(); if (heap.length > 0) { heap[0] = last; let i = 0; while (true) { let smallest = i; const l = i * 2 + 1; const r = i * 2 + 2; if (l < heap.length && heap[l].h < heap[smallest].h) smallest = l; if (r < heap.length && heap[r].h < heap[smallest].h) smallest = r; if (smallest === i) break; [heap[i], heap[smallest]] = [heap[smallest], heap[i]]; i = smallest; } } return top; } push(grid[sr][sc], sr, sc); visited[sr][sc] = true; let level = grid[sr][sc]; let ans = 0; while (heap.length > 0) { const cur = pop(); if (cur.h > level) level = cur.h; ans += level - cur.h; for (const [dx, dy] of dirs) { const nx = cur.x + dx; const ny = cur.y + dy; if (nx < 0 || ny < 0 || nx >= n || ny >= m || visited[nx][ny]) continue; visited[nx][ny] = true; push(grid[nx][ny], nx, ny); } } console.log(ans); });JS里最容易被忽视的是数字范围。JS的Number在超过2^53时会损失精度,这道题虽然通常数据不会那么大,但极端情况下累加水量可能达到10^12量级,仍然在安全范围,不用转BigInt。如果真碰到更大数据,建议使用BigInt,但会拖慢速度,能不用就不用。
JS的堆写法里push和pop都是常规操作,唯一要记得的是pop时先把堆顶和最后一个交换,再下滤。我用了解构赋值来交换,代码短一点,性能稍微差点但可以接受。
4. 机试实战中的坑与排查技巧
4.1 这个题最容易踩的5个坑
把能想到的坑先列成一张表,方便快速对照:
| 坑点 | 现象 | 解决办法 |
|---|---|---|
| 溃口在边界 | 答案一直不对,甚至全为0 | 先判断边界,直接在边界则输出0 |
| 入堆时未标记visited | 同一个点被反复push,堆越来越大,答案可能正确但严重超时 | 入堆的瞬间标记visited |
| 先累加ans再更新水位 | 低洼处的深度少算或多算 | 先更新waterLevel,再累加level - curH |
| 答案用int存储 | 数据一大就负数或溢出,结果全部错误 | 使用long/long long |
| 方向数组边界判断缺失 | 运行时数组越界或段错误 | 每个邻居都检查nx < 0 |
这5个坑里,“先累加ans再更新水位”是最隐蔽的。很多人看到水面高度比格子高度高,直接ans += level - curH,然后再判断curH是否大于level,结果水位永远不会上升,所有高处的格子都算成0积水,最终答案偏小。这个顺序问题必须刻在脑子里。
4.2 从报错到修复的真实记录
我拿自己实际调试时遇到的几种情况来说。
第一次写这题的时候,我用的是普通DFS,样例全过,一提交直接超时。原因就是忘了水位动态变化,DFS把同一片区域反复遍历了好几遍。后来改成优先队列BFS,超时问题才解决。这给我的教训是:碰到“水往低处流”的题,第一反应优先队列,别拿DFS硬扛。
第二次是答案偶尔少一点或者多一点,本地测试又很难复现。排查半天发现问题出在visited的标记上,我原本在弹出堆顶时才标记visited,结果同一个低洼点被四个邻居分别塞进堆里4次,每个都弹出并累加一次水量。把标记提前到入堆后,答案立刻稳定。
第三次是C语言版本,输出用的%d,答案超过了int范围,本地小数据全对,一到大数据就变成负数。因为C语言里printf("%d", longlong)不会报警告,但高位被截断,非常坑。检查所有输出格式串,把%d改成%lld,问题解决。
4.3 手写测试用例与对拍思路
考试环境里没有IDE调试,最好的办法是构造小样例手算。我常用的几个用例:
样例1,3×3网格,溃口在(1,1),高度如下:
9 9 9 9 1 8 9 8 9从(1,1)往四个方向看,(0,1)高度9,(1,0)高度9,(1,2)高度8,(2,1)高度8。所有邻居都比溃口高,水根本漫不出去,只淹溃口自己,答案0。这个用例用来验证“溃口高度高于四周时积水量为0”的逻辑。
样例2,同样3×3,但周围全低于溃口:
9 9 9 9 5 9 9 9 9溃口高度5,四周都是9。水位最高到9,溃口自身积水9-5=4。答案4。这条用例验证了单格洼地能正确计算。
样例3,一条低洼路径,应考虑更复杂的情况:
10 10 10 10 10 8 6 10 10 7 9 10 10 10 10 10溃口在(1,1)位置高度8。水会从8流到(2,1)高度7,再流到(1,2)高度6,但被(2,2)高度9挡住。最低出口高度是9,所以整个连通区域的水位最终到9。积水量=(9-8)+(9-7)+(9-6)=6。这个用例可以手动跑一遍,验证堆的处理顺序是否和手算一致。
对拍的办法其实更简单:把堆弹出的高度序列打印出来,和手算的水位提升过程对照。如果弹出的顺序不是严格非递减的,堆就写错了。这个技巧比随机对拍更直观,因为这道题的核心就是高度序列。
5. 从水库题到洪水填充一族:同类变体与进阶
5.1 常见变式与思路对照
水库溃坝填补的变式在机试里真的很多,整理几个典型的:
- 如果题目改成“输出最终水面高度”,那就在while循环里维护一个level变量,循环结束时输出的就是level。
- 如果溃口不是一个点,而是一条缝隙(一排坐标),就把这些坐标全部先塞进堆,再把level设成其中最低的那个高度。
- 如果题目问“最终淹没的面积”而不是体积,那就不累加level - curH,而是遇到curH <= level时把面积计数器加1。
- 如果水可以从网格边缘流走,那就先判断溃口是否和边缘连通,如果连通直接输出0,这是最简单的特判。
这些变式的核心都是同一套堆+BFS框架,只是统计口径变了。看懂本题之后,遇到这类“地形注水”题基本都能套。
5.2 数据量膨胀时的离线并查集方案
如果网格到了百万甚至千万级别,优先队列BFS的O(nmlog(n*m))就非常危险。一个更高级的离线方案是:把所有格子按高度从小到大排序,同时把若干个“询问点”(比如溃口)也按高度排序。然后用并查集维护连通块,从低到高逐步把格子“激活”并合并连通块。当溃口所在的连通块被激活到某一高度时,再通过预先计算好每个连通块的面积和高度差来统计水量。
具体细节这题用不上,但面试或机试加问“能否优化”时拿来当亮点提一嘴,是很加分的。核心思想说白了就是“把高度当时间轴,从低到高离线处理,用并查集代替在线堆”。
谈到实际备考,我的体会是:这种题最大的价值不是那几行AC代码,而是帮你建立起一套“什么时候用堆BFS”的条件反射。像这种带动态水位的二维积水题,遇到一次会了,后面同类的“接雨水II”“太平洋大西洋水流”等等就都不再是难题。如果你在考场上碰到它,先别急着写,手推一遍样例,把“水面到底怎么涨”想通了再动手,基本就稳了。