[Leetcode][407. 接雨水 II] 2种方法:优先队列存储每层围栏最低高度,BFS更新每个方块存水高度

By Long Luo

Leetcode 407. 接雨水 II 题目如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
407. 接雨水 II

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

示例 1:

![3D 接雨水 1](https://assets.leetcode.com/uploads/2021/04/08/trap1-3d.jpg)

输入: 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:

![3D 接雨水 2](https://assets.leetcode.com/uploads/2021/04/08/trap2-3d.jpg)

输入: 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
0 <= heightMap[i][j] <= 2 * 10^4

这道题是42. 接雨水 的3D版本,区别于2D版本只需要维护左右两个最高的木板,这里需要维护一个围栏,用堆(优先队列)来维护周围这个围栏中的最小元素。

方法一:优先队列 + 最小堆

思路与算法:

因为需要维护一个圈,用来存储水,所以需要从矩阵的最外围往里面遍历,不断缩小圈,直到遍历每个方块,在这个过程中计算每个方块能装多少水,更新 ans 。

那么什么方块能存水呢?

  • 该方块不是在圈外;
  • 该方块高度比其上下左右四个相邻的方块接水后的高度都要低。

假设方块的索引为(i,j)(i,j),方块高度为heightMap[i][j]\textit{heightMap}[i][j],方块接水后的高度为water[i][j]\textit{water}[i][j],则方块(i,j)(i,j)的接水后的高度为:

water[i][j]=max(heightMap[i][j],min(water[i1][j],water[i+1][j],water[i][j1],water[i][j+1]))\textit{water}[i][j] = \max(\textit{heightMap}[i][j],\min(\textit{water}[i-1][j],\textit{water}[i+1][j],\textit{water}[i][j-1],\textit{water}[i][j+1]))

每个方块(i,j)(i,j)实际接水量为:water[i][j]heightMap[i][j]\textit{water}[i][j] - \textit{heightMap}[i][j]

那问题变成了如何知道每个方块的water[i][j]\textit{water}[i][j]呢?

  1. 因为最外层的方块无法接水,所以最外层的方块接水后的高度water[i][j]=heightMap[i][j]\textit{water}[i][j]=\textit{heightMap}[i][j]

  2. 根据木桶原理,而每层水的高度则取决于每层方块中高度最低的方块;

  3. 从矩阵的四周边界开始,则可以知道最外层方块接水后的高度最小值。之后遍历每层中的每个方块周围44 个方块,如果比当前方块低,那么说明这个方块可以接水,接水后的高度就是当前层的最小高度,反之更高的话,将其加入优先队列中;

  4. 更新最外层的方块标记,在新的最外层的方块再次找到接水后的高度的最小值,依次迭代直到求出所有的方块的接水高度,即可知道矩阵中的接水容量。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public int trapRainWater(int[][] heightMap) {
if (heightMap == null || heightMap.length <= 1 || heightMap[0].length <= 1) {
return 0;
}

int ans = 0;
int row = heightMap.length;
int col = heightMap[0].length;

PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]);

boolean[][] visited = new boolean[row][col];
for (int i = 0; i < row; ++i) {
for (int j = 0; j < col; ++j) {
if (i == 0 || i == row - 1 || j == 0 || j == col - 1) {
pq.offer(new int[]{i, j, heightMap[i][j]});
visited[i][j] = true;
}
}
}

int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

while (!pq.isEmpty()) {
int[] curr = pq.poll();

for (int[] dir : dirs) {
int nextX = curr[0] + dir[0];
int nextY = curr[1] + dir[1];

if (nextX < 0 || nextX >= row || nextY < 0 || nextY >= col || visited[nextX][nextY]) {
continue;
}

if (heightMap[nextX][nextY] < curr[2]) {
ans += curr[2] - heightMap[nextX][nextY];
}

visited[nextX][nextY] = true;
pq.offer(new int[]{nextX, nextY, Math.max(heightMap[nextX][nextY], curr[2])});
}
}

return ans;
}

复杂度分析:

  • 时间复杂度:O(MNlog(M+N))O(MN\log(M+N)),其中MM是矩阵的行数,NN是矩阵的列数。我们需要将矩阵中的每个元素都进行遍历,同时将每个元素都需要插入到优先队列中,总共需要向队列中插入MNMN个元素,每次堆进行调整的时间复杂度为O(log(M+N))O(\log(M+N)),因此总的时间复杂度为O(MNlog(M+N))O(MN\log(M+N))
  • 空间复杂度:O(MN)O(MN),需要创建额外的空间对元素进行标记,优先队列中最多存储O(MN)O(MN)个元素,因此空间复杂度度为O(MN)O(MN)

方法二:BFS

思路与算法:

假设初始时矩阵的每个格子都接满了水,且高度均为maxHeight\textit{maxHeight},其中maxHeight\textit{maxHeight}为矩阵中高度最高的格子。

接水后的高度为water[i][j]\textit{water}[i][j],方块(i,j)(i,j)的接水后的高度为:

water[i][j]=max(heightMap[i][j],min(water[i1][j],water[i+1][j],water[i][j1],water[i][j+1]))\textit{water}[i][j] = \max(\textit{heightMap}[i][j],\min(\textit{water}[i-1][j],\textit{water}[i+1][j],\textit{water}[i][j-1],\textit{water}[i][j+1]))

假设每个方块(i,j)(i,j)的接水后的高度均为water[i][j]=maxHeight\textit{water}[i][j]=\textit{maxHeight},那么:

  1. 最外层的方块的肯定不能接水;
  2. 如果当前方块(i,j)(i,j)的接水高度water[i][j]\textit{water}[i][j]小于与它相邻的44个方块接水高度时,调整接水高度,将其相邻的4个方块的接水高度调整为与(i,j)(i,j)的高度一致;
  3. 不断重复进行调整,直到所有的方块的接水高度不再有调整时即为满足要求。

最后根据方块(i,j)(i,j)接水容量water[i][j]heightMap[i][j]\textit{water}[i][j] - \textit{heightMap}[i][j],遍历即为答案。

值得注意的是如果在BFS里运用visited标记,反而在特殊情况下失败,因为在这里BFS如果遇到一个更低的高度,需要将原来已经遍历过的高度也更新,所以不能使用visited标记。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public static int trapRainWater_bfs(int[][] heightMap) {
if (heightMap == null || heightMap.length <= 1 || heightMap[0].length <= 1) {
return 0;
}

int ans = 0;
int row = heightMap.length;
int col = heightMap[0].length;
int maxHeight = 0;
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
maxHeight = Math.max(maxHeight, heightMap[i][j]);
}
}
int[][] water = new int[row][col];
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
water[i][j] = maxHeight;
}
}

Queue<int[]> queue = new LinkedList<>();
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (i == 0 || i == row - 1 || j == 0 || j == col - 1) {
if (water[i][j] > heightMap[i][j]) {
water[i][j] = heightMap[i][j];
queue.offer(new int[]{i, j});
}
}
}
}

int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

while (!queue.isEmpty()) {
int[] curr = queue.poll();

for (int[] dir : dirs) {
int nextX = curr[0] + dir[0];
int nextY = curr[1] + dir[1];

if (nextX < 0 || nextX >= row || nextY < 0 || nextY >= col) {
continue;
}

// 只有比当前水位高才会流出水,water[nextX][nextY] > heightMap[nextX][nextY]是为了保证不会重复遍历。
if (water[curr[0]][curr[1]] < water[nextX][nextY] && water[nextX][nextY] > heightMap[nextX][nextY]) {
water[nextX][nextY] = Math.max(water[curr[0]][curr[1]], heightMap[nextX][nextY]);
queue.offer(new int[]{nextX, nextY});
}
}
}

for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
ans += water[i][j] - heightMap[i][j];
}
}

return ans;
}

复杂度分析:

  • 时间复杂度:O(M2N2)O(M^2N^2),其中MM是矩阵的行数,NN是矩阵的列数。每次发现有水面高度需要调整时,可能需要调整整个矩阵,因此时间复杂度为O(M2N2)O(M^2N^2)

  • 空间复杂度:O(MN)O(MN),需要创建额外的空间对每个元素进行标记,因此空间复杂度度为O(MN)O(MN)


All suggestions are welcome.
If you have any query or suggestion please comment below.
Please upvote👍 if you like💗 it. Thank you:-)

Explore More Leetcode Solutions. 😉😃💗