目描述
给一个 m x n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
Example 1:
输入: heightMap = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]]
输出: 4
解释: 下雨后,雨水将会被上图蓝色的方块中。总的接雨水量为1+2+1=4
Example 2:
输入: 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。
Input & Output
6
3 6
1 4 3 1 3 2
3 2 1 3 2 4
2 3 3 2 3 1
5 5
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
5 5
3 3 3 3 3
3 2 2 2 2
3 2 1 2 3
3 2 2 2 3
3 3 3 3 3
5 5
3 3 3 3 3
3 2 2 2 3
3 2 1 1 1
3 2 2 2 3
3 3 3 3 3
5 5
3 3 3 3 3
3 2 2 2 3
3 2 3 2 3
3 2 2 2 3
3 3 3 3 3
5 5
3 3 3 3 3
3 2 2 2 3
3 2 4 2 3
3 2 2 2 3
3 3 3 3 3
4
10
1
0
8
8
Solution
Solution_1(BFS)(Wrong)
遍历每个未被访问的点,数值小于等于该点的所有点接雨水后可以形成高度相同的水面,同一个水面的高度都是其四周方块最小的值,可以计算出每个水平面需要填补的雨水数量。
该方法会导致一个问题:当两个高度不同的水面相邻,本质上它们可以归为同一个水平面,但如果原本高度低的水平面先被遍历了,那么接到雨水的数量会变少。
特殊样例:
1
5 5
3 3 3 3 3
3 1 2 2 3
3 2 2 2 3
3 2 2 2 3
3 3 3 3 3
10
用次方法实现,输出错误结果9
import java.util.Scanner;
public class rain
{
static int T;
static int Row, Col;
static int[][] map;
static boolean[][] vis;
static int[][] dir = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
static int ans;
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
T = sc.nextInt();
for(int t=0; t<T; t++)
{
Row = sc.nextInt();
Col = sc.nextInt();
map = new int[Row][Col];
vis = new boolean[Row][Col];
ans = 0;
for(int i=0; i<Row; i++)
for(int j=0; j<Col; j++)
map[i][j] = sc.nextInt();
for(int i=1; i<Row-1; i++)
{
for(int j=1; j<Col-1; j++)
{
if(!vis[i][j])
{
ans += bfs(i, j);
}
}
}
System.out.println(ans);
}
sc.close();
}
private static int bfs(int row, int col)
{
boolean flag = false;
int min = Integer.MAX_VALUE;
int count = 0, sum = 0;
boolean[][] vis_1 = new boolean[Row][Col];
for(int i=1; i<Row-1; i++)
for(int j=1; j<Col-1; j++)
vis_1[i][j] = vis[i][j];
int[][] queue = new int [Row*Col][2];
int head = 0, tail = 0;
queue[tail][0] = row;
queue[tail][1] = col;
tail++;
vis_1[row][col] = true;
count++;
sum += map[row][col];
while(head < tail)
{
for(int i=0; i<dir.length; i++)
{
int next_row = queue[head][0] + dir[i][0];
int next_col = queue[head][1] + dir[i][1];
if(next_row>=0 && next_row<Row && next_col>=0 && next_col<Col)
{
int next_height = map[next_row][next_col];
if(next_height <= map[row][col])
{
if(next_row==0 || next_row==Row-1 || next_col==0 || next_col==Col-1)
return 0;
if(!vis_1[next_row][next_col])
{
queue[tail][0] = next_row;
queue[tail][1] = next_col;
tail++;
vis_1[next_row][next_col] = true;
count++;
sum += map[next_row][next_col];
}
}
else if(next_height < min)
{
flag = true;
min = next_height;
}
}
}
head++;
}
if(!flag)
return 0;
for(int i=1; i<Row-1; i++)
for(int j=1; j<Col-1; j++)
vis[i][j] = vis_1[i][j];
return min * count - sum;
}
}
可见,如果数值不同时两个高度的水平面相邻,会影响水平面划分的结果,可以考虑对内部的点先进行排序,从地势高的的开始划分同一水平面,可以避免上述情况。
Solution_2(暴力+队列)
思路:假设除了边界以外,每个点都存储了最大值。接下来遍历每个点,根据其四周高度最小的点计算该点要溢出的雨水量。
注意:每个点雨水的量改变之后都有可能会影响到其四周的点,所以将改变的点存入队列,然后挨个取出判断其四周的点是否会随之改变。
import java.util.Scanner;
public class rain
{
static int T;
static int Row, Col;
static int[][] map; //初始时格子的高度
static int[][] result_map; //存储了雨水之后的高度
static int[][] queue;
static int tail, head;
static int[][] dir = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
static long result; //存储的雨水量
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
T = sc.nextInt();
for(int t=0; t<T; t++)
{
Row = sc.nextInt();
Col = sc.nextInt();
map = new int[Row][Col];
result_map = new int[Row][Col];
result = 0;
for(int i=0; i<Row; i++)
{
for(int j=0; j<Col; j++)
{
map[i][j] = sc.nextInt();
if(IsBorder(i, j))
result_map[i][j] = map[i][j];
else //假设初始时,每格的雨水全部存到最大值
{
result_map[i][j] = Integer.MAX_VALUE;
result += Integer.MAX_VALUE - map[i][j];
}
}
}
//队列存储更新了高度的点
queue = new int[Row*Col][2];
tail = 0;
head = 0;
bfs();
System.out.println(result);
}
sc.close();
}
//判断该点是否是边界
private static boolean IsBorder(int row, int col)
{
if(row==0 || row==Row-1 || col==0 || col==Col-1)
return true;
return false;
}
private static void bfs()
{
//初次更新所有点的高度
for(int i=1; i<Row-1; i++)
for(int j=1; j<Col-1; j++)
result -= change(i ,j);
while(head != tail)
{
//取出高度改变的点
int row = queue[head][0];
int col = queue[head][1];
//遍历其四周可能产生影响的点
for(int i=0; i<dir.length; i++)
{
int next_row = row + dir[i][0];
int next_col = col + dir[i][1];
if(!IsBorder(next_row, next_col))
result -= change(next_row, next_col);
}
head = (head + 1) % (Row * Col);
}
}
//返回该点流出的雨水量,并把更新的点加入到队列
private static int change(int row, int col)
{
int flow = FlowCount(row, col);
if(flow > 0)
{
//如果计算出 流出了雨水之后的高度大于原本高度,
//说明该点低于四周的点,流出的量是根据四周的点计算的
if(result_map[row][col] - flow > map[row][col])
{
result_map[row][col] -= flow;
queue[tail][0] = row;
queue[tail][1] = col;
tail = (tail + 1) % (Row * Col);
}
//否则 说明该点高于四周的点,
//流出的雨水量只取决于该点本身的高度(初始时假定高度为MAX_VALUE)
else
{
flow = result_map[row][col] - map[row][col];
if(flow > 0)
{
result_map[row][col] = map[row][col];
queue[tail][0] = row;
queue[tail][1] = col;
tail = (tail + 1) % (Row * Col);
}
}
return flow;
}
return 0;
}
//根据四周的点,判断该点要流出的雨水量
private static int FlowCount(int row, int col)
{
int min = Integer.MAX_VALUE;
for(int i=0; i<dir.length; i++)
{
int next_row = row + dir[i][0];
int next_col = col + dir[i][1];
if(result_map[next_row][next_col] < min)
min = result_map[next_row][next_col];
}
int flow = result_map[row][col] - min;
return flow>0 ? flow : 0;
}
}
Solution_3(小根堆+木桶原理)
既然说到桶装水,那么本题显然和水桶原理有点关系:水桶的装水量由最低的那块木板决定
回到本体,显然边界的位置不能装水,那么它们就像是桶边,那么装水量显然和最低的那个位置有关系。也就是,不管内部的容器有多高或者多低,反正放的水的高度都受现在边界的最低高度限制。
那么显然,现在最低边界相邻的那个内部位置高度与边界高度的关系有:
- 边界高度大于内部位置。这种情况下,无论该位置其它三个方向的相邻高度为多少,该位置都可以放水,容量为 边界高度 - 内部位置高度
- 边界高度小于等于内部位置。这种情况下,该位置肯定不能放水
然后,不管该位置放水与否,我们都将其视为新的边界:
- 若它放水了,那么根据水桶原理,它相邻不会再有比当前位置更高的水面,于是它可以想象成一块和水面一样高的木板,也就是边界
- 若它没放水,肯定是高于原来的边界,就可以想象成一块钉在外部木板的内部木板,一个新边界
接下来,我们继续刚刚的操作,对下一个最低边界做这样的操作
于是可以使用堆存下边界的位置和高度的二元组。并标记放入的位置,防止重复放入
那么,每次都弹出堆顶元素,即还未处理的最低边界,然后进行上述操作,每次形成新边界时都放入堆,对于放入过堆的元素都进行标记
import java.util.Scanner;
public class rain
{
static int T;
static int Row, Col;
static int[][] map;
static boolean[][] vis;
static int[][] dir = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
static int result;
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
T = sc.nextInt();
for(int t=0; t<T; t++)
{
Row = sc.nextInt();
Col = sc.nextInt();
map = new int[Row][Col];
vis = new boolean[Row][Col];
result = 0;
Heap heap = new Heap(Row * Col);
for(int i=0; i<Row; i++)
{
for(int j=0; j<Col; j++)
{
map[i][j] = sc.nextInt();
if (i==0 || i==Row-1 || j==0 || j==Col-1)
{
vis[i][j] = true;
heap.push(i, j, map[i][j]);
}
}
}
// heap.Print();
while(heap.len > 0)
{
int x = heap.getX(0);
int y = heap.getY(0);
heap.pop();
for(int i=0; i<dir.length; i++)
{
int next_x = x + dir[i][0];
int next_y = y + dir[i][1];
if (next_x<=0 || next_x>=Row-1 || next_y<=0 || next_y>=Col-1 || vis[next_x][next_y])
continue;
int board_height = map[x][y];
int now_height = map[next_x][next_y];
int inject_rain = board_height - now_height;
if(inject_rain > 0)
{
result += inject_rain;
map[next_x][next_y] = board_height;
now_height = board_height;
}
heap.push(next_x, next_y, now_height);
vis[next_x][next_y] = true;
}
}
System.out.println(result);
}
sc.close();
}
}
//小根堆
class Heap
{
Node[] tree;
int len;
//开辟空间
Heap(int length)
{
this.tree = new Node[length];
this.len = 0;
}
//打印堆中元素
public void Print()
{
for(int i=0; i<len; i++)
System.out.print(tree[i].x + " ");
System.out.println();
for(int i=0; i<len; i++)
System.out.print(tree[i].y + " ");
System.out.println();
for(int i=0; i<len; i++)
System.out.print(tree[i].height + " ");
System.out.println();
}
//交换堆中两个下标标记的结点内容
void swap(int index1, int index2)
{
Node temp = new Node(tree[index1]);
tree[index1] = new Node(tree[index2]);
tree[index2] = temp;
}
//index1中结点的值是否比index2中结点的值小
boolean compare(int index1, int index2)
{
if(tree[index1].height < tree[index2].height)
return true;
return false;
}
//往堆中加入一个元素,并调整堆
void push(int x, int y, int height)
{
tree[len] = new Node(x, y, height);
int current = len;
int parent = (current - 1) / 2;
//从下往上调整,小于父节点的上移
while(current > 0)
{
if(compare(current, parent))
{
swap(current, parent);
current = parent;
parent = (current - 1) / 2;
}
else
break;
}
len++;
}
//由于无法同时返回两个数值,取出堆中最小元素时,调用三个函数体
int getX(int index)
{
return tree[index].x;
}
int getY(int index)
{
return tree[index].y;
}
//取出堆顶元素后,调整堆
void pop()
{
//实现堆顶元素和堆底元素位置交换,并从堆中取出最小值
len --;
tree[0] = new Node(tree[len]);
//从上到下调整堆
int current = 0;
int lChild = current * 2 + 1;
int rChild = current * 2 + 2;
while(current < len)
{
if(lChild >= len)
break;
//记录左右孩子中较小的结点
int minChild = lChild;
if(rChild<len && compare(rChild, minChild))
minChild = rChild;
//交换父子结点位置,把较大的结点挪到下面
if(compare(minChild, current))
{
swap(minChild, current);
current = minChild;
lChild = current * 2 + 1;
rChild = current * 2 + 2;
}
else
break;
}
}
}
//结点类
class Node
{
int x;
int y;
int height;
Node() {}
Node(Node node)
{
this.height = node.height;
this.x = node.x;
this.y = node.y;
}
Node(int x, int y, int height)
{
this.height = height;
this.x = x;
this.y = y;
}
}
Solution_4(大根堆+BFS,实现Solution_1)
遍历每个未被访问的点,数值小于等于该点的所有点接雨水后可以形成高度相同的水面,同一个水面的高度都是其四周方块最小的值,可以计算出每个水平面需要填补的雨水数量。
但如果数值不同时两个高度的水平面相邻,会影响水平面划分的结果,先使用堆对内部的点进行排序,从地势高的的开始划分同一水平面,可以避免影响水平面的划分。
import java.util.Scanner;
//结点类
class Node
{
int x;
int y;
int height;
Node() {}
Node(Node node)
{
this.x = node.x;
this.y = node.y;
this.height = node.height;
}
Node(int x, int y, int height)
{
this.x = x;
this.y = y;
this.height = height;
}
}
//大根堆
class Heap
{
Node[] tree;
int len;
//构造函数
Heap() {}
//开辟空间
Heap(int length)
{
tree = new Node[length];
len = 0;
}
//打印堆中元素
public void Print()
{
for(int i=0; i<len; i++)
System.out.print(tree[i].x + " ");
System.out.println();
for(int i=0; i<len; i++)
System.out.print(tree[i].y + " ");
System.out.println();
for(int i=0; i<len; i++)
System.out.print(tree[i].height + " ");
System.out.println();
System.out.println();
}
//交换堆中两个下标标记的结点内容
void swap(int index1, int index2)
{
Node temp = new Node(tree[index1]);
tree[index1] = new Node(tree[index2]);
tree[index2] = temp;
}
//index1中结点的值是否比index2中结点的值大
boolean compare(int index1, int index2)
{
if(tree[index1].height > tree[index2].height)
return true;
return false;
}
//往堆中加入一个元素,并调整堆
void push(int x, int y, int height)
{
tree[len] = new Node(x, y, height);
int current = len;
int parent = (current - 1) / 2;
//从下往上调整,大于父节点的结点上移
while(current > 0)
{
if(compare(current, parent))
{
swap(current, parent);
current = parent;
parent = (current - 1) / 2;
}
else
break;
}
len++;
}
//由于无法同时返回两个数值,取出堆中最大元素时,需要调用三个函数体
int getX(int index)
{
return tree[index].x;
}
int getY(int index)
{
return tree[index].y;
}
//取出堆顶元素后,调整堆
void pop()
{
//实现堆顶元素和堆底元素位置交换,并从堆中取出最大值
len--;
tree[0] = new Node(tree[len]);
//从上到下调整堆
int current = 0;
int lChild = current * 2 + 1;
int rChild = current * 2 + 2;
while(lChild < len)
{
//记录左右孩子中较小的结点
int minChild = lChild;
if(rChild<len && compare(rChild, minChild))
minChild = rChild;
//交换父子结点位置,把较小的结点挪到下面
if(compare(minChild, current))
{
swap(minChild, current);
current = minChild;
lChild = current * 2 + 1;
rChild = current * 2 + 2;
}
else
break;
}
}
}
public class rain
{
static int T;
static int Row, Col;
static int[][] map;
//所有可能存储雨水的格子按高度降序排序
static Heap heap;
//已经接了雨水的格子被标为true
static boolean[][] vis_raw;
static int[][] dir = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
static int ans;
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
T = sc.nextInt();
for(int t=0; t<T; t++)
{
Row = sc.nextInt();
Col = sc.nextInt();
map = new int[Row][Col];
heap = new Heap(Row * Col);
vis_raw = new boolean[Row][Col];
ans = 0;
for(int i=0; i<Row; i++)
{
for(int j=0; j<Col; j++)
{
map[i][j] = sc.nextInt();
if (i==0 || i==Row-1 || j==0 || j==Col-1)
continue;
//除了边界外的格子全都进入堆
heap.push(i, j, map[i][j]);
}
}
// heap.Print();
bfs();
System.out.println(ans);
}
sc.close();
}
private static void bfs()
{
while(heap.len > 0)
{
//由高到低取出堆中还没有存雨水的点
int row = heap.getX(0);
int col = heap.getY(0);
heap.pop();
if(vis_raw[row][col])
continue;
//水面四周最矮的边界
int min = Integer.MAX_VALUE;
int count = 0, sum = 0; //接雨水的格子个数、底部总共的格子数,用于计算该水面区域接到的雨水
//拷贝接雨水的区域,如果发现边界太低无法接水,可以免于回溯
boolean[][] vis = new boolean[Row][Col];
for(int i=1; i<Row-1; i++)
for(int j=0; j<Col-1; j++)
vis[i][j] = vis_raw[i][j];
//bfs核心:将可以归为同一水平面的点入队,并进行宽搜
int[][] queue = new int[Row * Col][2];
int head = 0, tail = 0;
queue[tail][0] = row;
queue[tail][1] = col;
tail++;
vis[row][col] = true;
count++;
int cur_height = map[row][col];
sum += cur_height;
//如果一个平面到了四周,高度还是低于当前点的高度,说明接不住雨水
boolean leak = false;
while(head<tail && !leak)
{
for(int i=0; i<dir.length; i++)
{
int next_row = queue[head][0] + dir[i][0];
int next_col = queue[head][1] + dir[i][1];
//因为加入队列中的点都不是边界,所以不会有数组越界的情况,无需判断
int next_height = map[next_row][next_col];
//该点四周的点地势比当前点更低,存在两种情况:
//1. 四周的点位于边缘,该区域无法接水,会漏
//2. 四周的点不在边缘,也是同一水平面的点,加入队列
if(next_height <= cur_height)
{
if(next_row==0 || next_row==Row-1 || next_col==0 || next_col==Col-1)
{
leak = true;
break;
}
if(!vis[next_row][next_col])
{
queue[tail][0] = next_row;
queue[tail][1] = next_col;
tail++;
vis[next_row][next_col] = true;
count++;
sum += next_height;
}
}
//该点四周的点地势比当前点更高,判断是否可以更新边界的最小值
else if(next_height < min)
min = next_height;
}
head++;
}
//可以接水
if(!leak)
{
//把当前已经接雨水的点更新到全局
for(int i=1; i<Row-1; i++)
for(int j=1; j<Col-1; j++)
vis_raw[i][j] = vis[i][j];
//计算当前水平面接到的雨水,更新到结果
int rain = min * count - sum;
ans += rain;
}
}
}
}