接雨水II
class Solution {
static constexpr int DIRS[4][2] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
public:
int trapRainWater(vector<vector<int>>& heightMap) {
int m = heightMap.size(), n = heightMap[0].size();
priority_queue<tuple<int, int, int>, vector<tuple<int, int, int>>, greater<>> pq;
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
if (i == 0 || j == 0 || i == m - 1 || j == n - 1)
{
pq.emplace(heightMap[i][j], i, j);
heightMap[i][j] = -1;
}
}
}
int ans = 0;
while (!pq.empty())
{
auto [dig, i, j] = pq.top();
pq.pop();
for (auto& [dx, dy] : DIRS)
{
int nx = i + dx, ny = j + dy;
if (0 <= nx && nx < m && 0 <= ny && ny < n && heightMap[nx][ny] >= 0)
{
ans += max(dig - heightMap[nx][ny], 0);
pq.emplace(max(dig, heightMap[nx][ny]), nx, ny);
heightMap[nx][ny] = -1;
}
}
}
return ans;
}
};
📝 算法笔记:3D 接雨水 (Trapping Rain Water II)
1. 算法分类与核心思想
- 算法分类:优先队列 (Priority Queue) + BFS,也常被称为“Dijkstra-like BFS”。
- 核心原理:木桶效应 (Wood Bucket Theory)
- 水能蓄多高,不取决于周围最高的板,而是取决于最低的那块木板。
- 为了找到这个“最低木板”,我们使用最小堆(Min-Heap)始终处理当前边界中最矮的格子。
2. 代码逻辑三步走
第一步:找边界(初始化)
- 把矩阵最外圈的所有格子全部放入最小堆
pq。 - 最外圈的格子是装不住水的,它们是最初的“木桶壁”。
- 将处理过的格子标记为
-1(防止重复访问)。
第二步:由外向内渗透(贪心搜索)
- 每次从堆中弹出高度最低的格子
(dig, i, j)。 - 贪心逻辑:既然它是目前围墙里最矮的,那么它能承载的水位就决定了它邻居的蓄水上限。
第三步:填坑与更新围墙
遍历当前格子的邻居 (nx, ny):
- 算水量:如果邻居比
dig矮,说明邻居是个坑。接水量 = max(0, dig - 邻居高度)。
- 移围墙:邻居被处理后,就成了新的围墙。
- 新高度更新:
max(dig, 邻居高度)。 - 解释:如果邻居矮,填水后高度变成
dig;如果邻居高,它本身就是更高的新围墙。
- 新高度更新:
- 入堆:把新高度和坐标放入堆中,继续循环。
3. 关键代码片段解析
-
最小堆定义:
priority_queue<..., greater<>> pq; —— 必须是 greater,保证弹出的是最矮的板。
-
蓄水公式:
ans += max(dig - heightMap[nx][ny], 0); —— 只有内部比边界低才能接水。
-
边界更新(核心):
pq.emplace(max(dig, heightMap[nx][ny]), nx, ny); —— 无论接不接水,邻居都变成了新围墙的一部分,其高度是“注水后”的高度。
4. 复杂度与注意事项
- 时间复杂度:$O(MN \log(MN))$。每个格子进出堆一次,堆操作为对数级别。
- 空间复杂度:$O(MN)$。用于堆存储和原数组标记。
- 易错点:
- 行列对应:
m对应heightMap.size()(行),n对应heightMap[0].size()(列)。 - 标记位:记得给访问过的格子打标记(如
=-1),否则会陷入死循环。 - 长方形边界:判断边界时
i == m - 1或j == n - 1不要写混。
- 行列对应:
5. 总结口诀
最矮边界先出堆,看向邻居填水坑。
水满之后围墙移,高度取大再入堆。
浙公网安备 33010602011771号