目描述
给一个 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
63 61 4 3 1 3 23 2 1 3 2 42 3 3 2 3 15 53 3 3 3 33 2 2 2 33 2 1 2 33 2 2 2 33 3 3 3 35 53 3 3 3 33 2 2 2 23 2 1 2 33 2 2 2 33 3 3 3 35 53 3 3 3 33 2 2 2 33 2 1 1 13 2 2 2 33 3 3 3 35 53 3 3 3 33 2 2 2 33 2 3 2 33 2 2 2 33 3 3 3 35 53 3 3 3 33 2 2 2 33 2 4 2 33 2 2 2 33 3 3 3 3
4101088
Solution
Solution_1(BFS)(Wrong)
遍历每个未被访问的点,数值小于等于该点的所有点接雨水后可以形成高度相同的水面,同一个水面的高度都是其四周方块最小的值,可以计算出每个水平面需要填补的雨水数量。
该方法会导致一个问题:当两个高度不同的水面相邻,本质上它们可以归为同一个水平面,但如果原本高度低的水平面先被遍历了,那么接到雨水的数量会变少。
特殊样例:
15 53 3 3 3 33 1 2 2 33 2 2 2 33 2 2 2 33 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;}elsebreak;}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;}elsebreak;}}}//结点类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;}elsebreak;}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;}elsebreak;}}}public class rain{static int T;static int Row, Col;static int[][] map;//所有可能存储雨水的格子按高度降序排序static Heap heap;//已经接了雨水的格子被标为truestatic 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;}}}}
