0
点赞
收藏
分享

微信扫一扫

407. 接雨水 II : 经典 Dijkstra 运用题


题目描述

这是 LeetCode 上的 ​​407. 接雨水 II​​ ,难度为 困难

Tag : 「图论最短路」、「优先队列(堆)」

给你一个  的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。

示例 1: 407. 接雨水 II : 经典 Dijkstra 运用题_优先队列

输入: heightMap = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]]

输出: 4

解释: 下雨后,雨水将会被上图蓝色的方块中。总的接雨水量为1+2+1=4。

示例 2: 407. 接雨水 II : 经典 Dijkstra 运用题_优先队列_02

输入: heightMap = [[3,3,3,3,3],[3,2,2,2,3],[3,2,1,2,3],[3,2,2,2,3],[3,3,3,3,3]]

输出: 10

提示:

  • m == heightMap.length
  • n == heightMap[i].length
  • 1 <= m, n <= 200

Dijkstra 变形 + 优先队列(堆)

首先,最外层的一圈(边界)是不会接到任何雨水的(会从边界流出)。

我们定义从点 到边界的路径中出现的最大高度为「路径高度」,路径高度 必然满足 。

问题的本质是求「从点 到边界的所有路径高度的最小值为多少」,这个路径高度的最小值与 本身的高度 之间的差值,即是该点能接到的雨水数量。


PS. 对此觉得不好理解的同学,可以尝试从现实角度出发进行考虑:假如该位置是出水口(能够源源不断的出水并往各个方向蔓延),那么该位置最终承载多少水,取决于所有路径高度中的最小值(水流最终会被该路径高度的最小值所拦截),即出水口最终形成的水量为「路径高度最小值」与「出水口起始高度」之间的差值。


我们考虑如何计算某个位置 的所有路径高度中的最小值。

如果是求从 出发的所有路径中,路径总和的最小值,那么可以直接使用 Dijkstra 等单源最短路算法来进行求解。

本题求的是所有路径中,路径高度的最小值,需要对 Dijkstra 进行变形。

首先「从 出发到达边界的路径」也可看作「从边界到达点 的路径」,经过转换操作后,直接计算边界到点 的路径是一个多源点问题。

我们可以考虑引入「超级源点」进行简化:超级源点与所有的边界格子连有一条权值为 的边,从而进一步将问题转化为「求从超级源点出发到达 的路径高度的最小值」。

与求最短路的 Dijkstra 类似,我们可以将「使用出队元素更新邻点的松弛操作」等价「使用出队元素更新相邻格子的雨水量」。

如果我们能够保证被出队元素所更新的高度为最终高度(或者说出队元素的高度为最终高度),那么该做法的正确性就能被 Dijkstra 的正确性所保证。

我们知道,对于边界格子而言,由于其不能接到任何雨水(即最终高度为起始高度),因此它们可以先进行入队。

然后考虑如何出队并更新邻点可以确保正确性。

对于某个位置 而言,根据「木桶原理」,其最终高度取决于四个方向的邻点的最终高度的最小值。

这引导我们使用优先队列(堆)来做:建立一个小根堆,存储 三元组信息( 为 位置 的最终高度),每次使用出队元素来更新邻点,由于是小根堆,可以确保出队元素是被更新的元素的最小高度的邻点,并且被更新元素会被更新为最终高度,然后将被更新元素进行入队,重复此过程,直到所有位置均被更新。


实现上,我们不需要真将超级源点建立出来,只需要起始将所有边界点放入优先队列即可。同时由于每个点只有被第一个出队元素所更新的高度为最终高度,我们还需要使用 数组来避免重复更新。


代码:

class Solution {
public int trapRainWater(int[][] heightMap) {
int m = heightMap.length, n = heightMap[0].length;
PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[2]-b[2]);
boolean[][] vis = new boolean[m][n];
for (int i = 0; i < n; i++) {
q.add(new int[]{0, i, heightMap[0][i]});
q.add(new int[]{m - 1, i, heightMap[m - 1][i]});
vis[0][i] = vis[m - 1][i] = true;
}
for (int i = 1; i < m - 1; i++) {
q.add(new int[]{i, 0, heightMap[i][0]});
q.add(new int[]{i, n - 1, heightMap[i][n - 1]});
vis[i][0] = vis[i][n - 1] = true;
}
int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
int ans = 0;
while (!q.isEmpty()) {
int[] poll = q.poll();
int x = poll[0], y = poll[1], h = poll[2];
for (int[] d : dirs) {
int nx = x + d[0], ny = y + d[1];
if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
if (vis[nx][ny]) continue;
if (h > heightMap[nx][ny]) ans += h - heightMap[nx][ny];
q.add(new int[]{nx, ny, Math.max(heightMap[nx][ny], h)});
vis[nx][ny] = true;
}
}
return ans;
}
}
  • 时间复杂度:所有的点均进出一次优先队列(堆),复杂度为
  • 空间复杂度:

最后

这是我们「刷穿 LeetCode」系列文章的第 ​​No.407​​ 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:​​github.com/SharingSour…​​ 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

举报

相关推荐

0 条评论