题目来源:https://leetcode-cn.com/problems/trapping-rain-water-ii/
大致题意:
给定一个二维数组。把这个数组想象成一个不规则的桶,每个元素的值可以想象成该位置的木板高度。求出这个桶可以装多少容量的水
比如:
3, 3, 3, 3
3, 2, 1, 3
3, 1, 2, 3
3, 4, 4, 4
可以看到中间的
2,1
1,2
就是桶内部可以装水的位置,因为它的边界比这部分都高,那么对应位置可以装水
1,2
2,1
装完之后的高度为
3, 3, 3, 3
3, 3, 3, 3
3, 3, 3, 3
3, 4, 4, 4
思路
既然说到桶装水,那么本题显然和水桶原理有点关系:水桶的装水量由最低的那块木板决定
回到本体,显然边界的位置不能装水,那么它们就像是桶边,那么装水量显然和最低的那个位置有关系。也就是,不管内部的容器有多高或者多低,反正放的水的高度都受现在边界的最低高度限制。
那么显然,现在最低边界相邻的那个内部位置高度与边界高度的关系有:
- 边界高度大于内部位置。这种情况下,无论该位置其它三个方向的相邻高度为多少,该位置都可以放水,容量为 边界高度 - 内部位置高度
- 边界高度小于等于内部位置。这种情况下,该位置肯定不能放水
然后,不管该位置放水与否,我们都将其视为新的边界:
- 若它放水了,那么根据水桶原理,它相邻不会再有比当前位置更高的水面,于是它可以想象成一块和水面一样高的木板,也就是边界
- 若它没放水,肯定是高于原来的边界,就可以想象成一块钉在外部木板的内部木板,一个新边界
接下来,我们继续刚刚的操作,对下一个最低边界做这样的操作
于是可以使用优先队列存下边界的位置和高度的二元组。并标记放入的位置,防止重复放入
那么,每次都弹出队首元素,即还未处理的最低边界,然后进行上述操作,每次形成新边界时都放入队列,对于放入过队列的元素都进行标记
代码:
public int trapRainWater(int[][] heightMap) {
int m = heightMap.length;
int n = heightMap[0].length;
// 只有桶边,没有内容
if (m <= 2 || n <= 2) {
return 0;
}
// 用来存储边界 (坐标,高度)
PriorityQueue<int[]> border = new PriorityQueue<>((o1, o2) -> o1[1] - o2[1]);
// 用来标记访问过的位置,也就是被认为为边界的位置
boolean[][] vis = new boolean[m][n];
// 用来取出上下左右四个方向
int[] direct = new int[]{-1, 0, 1, 0, -1};
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 将边界放入堆,并标记
if (i == 0 || i == m - 1 || j == 0 || j == n - 1) {
// 坐标转换成一维
border.add(new int[]{i * n + j, heightMap[i][j]});
vis[i][j] = true;
}
}
}
int ans = 0;
// 遍历所有格子
while (!border.isEmpty()) {
// 取出当前的最低边界
int[] cur = border.poll();
// 遍历该边界四个方向中还未访问的格子
for (int i = 0; i < 4; i++) {
int nx = cur[0] / n + direct[i];
int ny = cur[0] % n + direct[i + 1];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && !vis[nx][ny]) {
// 如果边界大于该格子,那么该格子可以放雨水
if (cur[1] > heightMap[nx][ny]) {
ans += cur[1] - heightMap[nx][ny];
}
// 将该格子放入堆,更新为新边界
// 边界值为 格子高度 与 当前边界 之间的最大值
border.add(new int[]{nx * n + ny, Math.max(heightMap[nx][ny], cur[1])});
vis[nx][ny] = true;
}
}
}
return ans;
}