目描述

给一个 m x n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
Example 1:
image.png
输入: 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:
image.png
输入: 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

  1. 6
  2. 3 6
  3. 1 4 3 1 3 2
  4. 3 2 1 3 2 4
  5. 2 3 3 2 3 1
  6. 5 5
  7. 3 3 3 3 3
  8. 3 2 2 2 3
  9. 3 2 1 2 3
  10. 3 2 2 2 3
  11. 3 3 3 3 3
  12. 5 5
  13. 3 3 3 3 3
  14. 3 2 2 2 2
  15. 3 2 1 2 3
  16. 3 2 2 2 3
  17. 3 3 3 3 3
  18. 5 5
  19. 3 3 3 3 3
  20. 3 2 2 2 3
  21. 3 2 1 1 1
  22. 3 2 2 2 3
  23. 3 3 3 3 3
  24. 5 5
  25. 3 3 3 3 3
  26. 3 2 2 2 3
  27. 3 2 3 2 3
  28. 3 2 2 2 3
  29. 3 3 3 3 3
  30. 5 5
  31. 3 3 3 3 3
  32. 3 2 2 2 3
  33. 3 2 4 2 3
  34. 3 2 2 2 3
  35. 3 3 3 3 3
  1. 4
  2. 10
  3. 1
  4. 0
  5. 8
  6. 8

Solution

Solution_1(BFS)(Wrong)

遍历每个未被访问的点,数值小于等于该点的所有点接雨水后可以形成高度相同的水面,同一个水面的高度都是其四周方块最小的值,可以计算出每个水平面需要填补的雨水数量。
该方法会导致一个问题:当两个高度不同的水面相邻,本质上它们可以归为同一个水平面,但如果原本高度低的水平面先被遍历了,那么接到雨水的数量会变少。
特殊样例:

  1. 1
  2. 5 5
  3. 3 3 3 3 3
  4. 3 1 2 2 3
  5. 3 2 2 2 3
  6. 3 2 2 2 3
  7. 3 3 3 3 3
  1. 10

用次方法实现,输出错误结果9

  1. import java.util.Scanner;
  2. public class rain
  3. {
  4. static int T;
  5. static int Row, Col;
  6. static int[][] map;
  7. static boolean[][] vis;
  8. static int[][] dir = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
  9. static int ans;
  10. public static void main(String[] args)
  11. {
  12. Scanner sc = new Scanner(System.in);
  13. T = sc.nextInt();
  14. for(int t=0; t<T; t++)
  15. {
  16. Row = sc.nextInt();
  17. Col = sc.nextInt();
  18. map = new int[Row][Col];
  19. vis = new boolean[Row][Col];
  20. ans = 0;
  21. for(int i=0; i<Row; i++)
  22. for(int j=0; j<Col; j++)
  23. map[i][j] = sc.nextInt();
  24. for(int i=1; i<Row-1; i++)
  25. {
  26. for(int j=1; j<Col-1; j++)
  27. {
  28. if(!vis[i][j])
  29. {
  30. ans += bfs(i, j);
  31. }
  32. }
  33. }
  34. System.out.println(ans);
  35. }
  36. sc.close();
  37. }
  38. private static int bfs(int row, int col)
  39. {
  40. boolean flag = false;
  41. int min = Integer.MAX_VALUE;
  42. int count = 0, sum = 0;
  43. boolean[][] vis_1 = new boolean[Row][Col];
  44. for(int i=1; i<Row-1; i++)
  45. for(int j=1; j<Col-1; j++)
  46. vis_1[i][j] = vis[i][j];
  47. int[][] queue = new int [Row*Col][2];
  48. int head = 0, tail = 0;
  49. queue[tail][0] = row;
  50. queue[tail][1] = col;
  51. tail++;
  52. vis_1[row][col] = true;
  53. count++;
  54. sum += map[row][col];
  55. while(head < tail)
  56. {
  57. for(int i=0; i<dir.length; i++)
  58. {
  59. int next_row = queue[head][0] + dir[i][0];
  60. int next_col = queue[head][1] + dir[i][1];
  61. if(next_row>=0 && next_row<Row && next_col>=0 && next_col<Col)
  62. {
  63. int next_height = map[next_row][next_col];
  64. if(next_height <= map[row][col])
  65. {
  66. if(next_row==0 || next_row==Row-1 || next_col==0 || next_col==Col-1)
  67. return 0;
  68. if(!vis_1[next_row][next_col])
  69. {
  70. queue[tail][0] = next_row;
  71. queue[tail][1] = next_col;
  72. tail++;
  73. vis_1[next_row][next_col] = true;
  74. count++;
  75. sum += map[next_row][next_col];
  76. }
  77. }
  78. else if(next_height < min)
  79. {
  80. flag = true;
  81. min = next_height;
  82. }
  83. }
  84. }
  85. head++;
  86. }
  87. if(!flag)
  88. return 0;
  89. for(int i=1; i<Row-1; i++)
  90. for(int j=1; j<Col-1; j++)
  91. vis[i][j] = vis_1[i][j];
  92. return min * count - sum;
  93. }
  94. }

可见,如果数值不同时两个高度的水平面相邻,会影响水平面划分的结果,可以考虑对内部的点先进行排序,从地势高的的开始划分同一水平面,可以避免上述情况。

Solution_2(暴力+队列)

思路:假设除了边界以外,每个点都存储了最大值。接下来遍历每个点,根据其四周高度最小的点计算该点要溢出的雨水量。
注意:每个点雨水的量改变之后都有可能会影响到其四周的点,所以将改变的点存入队列,然后挨个取出判断其四周的点是否会随之改变。

  1. import java.util.Scanner;
  2. public class rain
  3. {
  4. static int T;
  5. static int Row, Col;
  6. static int[][] map; //初始时格子的高度
  7. static int[][] result_map; //存储了雨水之后的高度
  8. static int[][] queue;
  9. static int tail, head;
  10. static int[][] dir = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
  11. static long result; //存储的雨水量
  12. public static void main(String[] args)
  13. {
  14. Scanner sc = new Scanner(System.in);
  15. T = sc.nextInt();
  16. for(int t=0; t<T; t++)
  17. {
  18. Row = sc.nextInt();
  19. Col = sc.nextInt();
  20. map = new int[Row][Col];
  21. result_map = new int[Row][Col];
  22. result = 0;
  23. for(int i=0; i<Row; i++)
  24. {
  25. for(int j=0; j<Col; j++)
  26. {
  27. map[i][j] = sc.nextInt();
  28. if(IsBorder(i, j))
  29. result_map[i][j] = map[i][j];
  30. else //假设初始时,每格的雨水全部存到最大值
  31. {
  32. result_map[i][j] = Integer.MAX_VALUE;
  33. result += Integer.MAX_VALUE - map[i][j];
  34. }
  35. }
  36. }
  37. //队列存储更新了高度的点
  38. queue = new int[Row*Col][2];
  39. tail = 0;
  40. head = 0;
  41. bfs();
  42. System.out.println(result);
  43. }
  44. sc.close();
  45. }
  46. //判断该点是否是边界
  47. private static boolean IsBorder(int row, int col)
  48. {
  49. if(row==0 || row==Row-1 || col==0 || col==Col-1)
  50. return true;
  51. return false;
  52. }
  53. private static void bfs()
  54. {
  55. //初次更新所有点的高度
  56. for(int i=1; i<Row-1; i++)
  57. for(int j=1; j<Col-1; j++)
  58. result -= change(i ,j);
  59. while(head != tail)
  60. {
  61. //取出高度改变的点
  62. int row = queue[head][0];
  63. int col = queue[head][1];
  64. //遍历其四周可能产生影响的点
  65. for(int i=0; i<dir.length; i++)
  66. {
  67. int next_row = row + dir[i][0];
  68. int next_col = col + dir[i][1];
  69. if(!IsBorder(next_row, next_col))
  70. result -= change(next_row, next_col);
  71. }
  72. head = (head + 1) % (Row * Col);
  73. }
  74. }
  75. //返回该点流出的雨水量,并把更新的点加入到队列
  76. private static int change(int row, int col)
  77. {
  78. int flow = FlowCount(row, col);
  79. if(flow > 0)
  80. {
  81. //如果计算出 流出了雨水之后的高度大于原本高度,
  82. //说明该点低于四周的点,流出的量是根据四周的点计算的
  83. if(result_map[row][col] - flow > map[row][col])
  84. {
  85. result_map[row][col] -= flow;
  86. queue[tail][0] = row;
  87. queue[tail][1] = col;
  88. tail = (tail + 1) % (Row * Col);
  89. }
  90. //否则 说明该点高于四周的点,
  91. //流出的雨水量只取决于该点本身的高度(初始时假定高度为MAX_VALUE)
  92. else
  93. {
  94. flow = result_map[row][col] - map[row][col];
  95. if(flow > 0)
  96. {
  97. result_map[row][col] = map[row][col];
  98. queue[tail][0] = row;
  99. queue[tail][1] = col;
  100. tail = (tail + 1) % (Row * Col);
  101. }
  102. }
  103. return flow;
  104. }
  105. return 0;
  106. }
  107. //根据四周的点,判断该点要流出的雨水量
  108. private static int FlowCount(int row, int col)
  109. {
  110. int min = Integer.MAX_VALUE;
  111. for(int i=0; i<dir.length; i++)
  112. {
  113. int next_row = row + dir[i][0];
  114. int next_col = col + dir[i][1];
  115. if(result_map[next_row][next_col] < min)
  116. min = result_map[next_row][next_col];
  117. }
  118. int flow = result_map[row][col] - min;
  119. return flow>0 ? flow : 0;
  120. }
  121. }

Solution_3(小根堆+木桶原理)

既然说到桶装水,那么本题显然和水桶原理有点关系:水桶的装水量由最低的那块木板决定
回到本体,显然边界的位置不能装水,那么它们就像是桶边,那么装水量显然和最低的那个位置有关系。也就是,不管内部的容器有多高或者多低,反正放的水的高度都受现在边界的最低高度限制。
那么显然,现在最低边界相邻的那个内部位置高度与边界高度的关系有:

  • 边界高度大于内部位置。这种情况下,无论该位置其它三个方向的相邻高度为多少,该位置都可以放水,容量为 边界高度 - 内部位置高度
  • 边界高度小于等于内部位置。这种情况下,该位置肯定不能放水

然后,不管该位置放水与否,我们都将其视为新的边界:

  • 若它放水了,那么根据水桶原理,它相邻不会再有比当前位置更高的水面,于是它可以想象成一块和水面一样高的木板,也就是边界
  • 若它没放水,肯定是高于原来的边界,就可以想象成一块钉在外部木板的内部木板,一个新边界

接下来,我们继续刚刚的操作,对下一个最低边界做这样的操作
于是可以使用堆存下边界的位置和高度的二元组。并标记放入的位置,防止重复放入
那么,每次都弹出堆顶元素,即还未处理的最低边界,然后进行上述操作,每次形成新边界时都放入堆,对于放入过堆的元素都进行标记

  1. import java.util.Scanner;
  2. public class rain
  3. {
  4. static int T;
  5. static int Row, Col;
  6. static int[][] map;
  7. static boolean[][] vis;
  8. static int[][] dir = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
  9. static int result;
  10. public static void main(String[] args)
  11. {
  12. Scanner sc = new Scanner(System.in);
  13. T = sc.nextInt();
  14. for(int t=0; t<T; t++)
  15. {
  16. Row = sc.nextInt();
  17. Col = sc.nextInt();
  18. map = new int[Row][Col];
  19. vis = new boolean[Row][Col];
  20. result = 0;
  21. Heap heap = new Heap(Row * Col);
  22. for(int i=0; i<Row; i++)
  23. {
  24. for(int j=0; j<Col; j++)
  25. {
  26. map[i][j] = sc.nextInt();
  27. if (i==0 || i==Row-1 || j==0 || j==Col-1)
  28. {
  29. vis[i][j] = true;
  30. heap.push(i, j, map[i][j]);
  31. }
  32. }
  33. }
  34. // heap.Print();
  35. while(heap.len > 0)
  36. {
  37. int x = heap.getX(0);
  38. int y = heap.getY(0);
  39. heap.pop();
  40. for(int i=0; i<dir.length; i++)
  41. {
  42. int next_x = x + dir[i][0];
  43. int next_y = y + dir[i][1];
  44. if (next_x<=0 || next_x>=Row-1 || next_y<=0 || next_y>=Col-1 || vis[next_x][next_y])
  45. continue;
  46. int board_height = map[x][y];
  47. int now_height = map[next_x][next_y];
  48. int inject_rain = board_height - now_height;
  49. if(inject_rain > 0)
  50. {
  51. result += inject_rain;
  52. map[next_x][next_y] = board_height;
  53. now_height = board_height;
  54. }
  55. heap.push(next_x, next_y, now_height);
  56. vis[next_x][next_y] = true;
  57. }
  58. }
  59. System.out.println(result);
  60. }
  61. sc.close();
  62. }
  63. }
  64. //小根堆
  65. class Heap
  66. {
  67. Node[] tree;
  68. int len;
  69. //开辟空间
  70. Heap(int length)
  71. {
  72. this.tree = new Node[length];
  73. this.len = 0;
  74. }
  75. //打印堆中元素
  76. public void Print()
  77. {
  78. for(int i=0; i<len; i++)
  79. System.out.print(tree[i].x + " ");
  80. System.out.println();
  81. for(int i=0; i<len; i++)
  82. System.out.print(tree[i].y + " ");
  83. System.out.println();
  84. for(int i=0; i<len; i++)
  85. System.out.print(tree[i].height + " ");
  86. System.out.println();
  87. }
  88. //交换堆中两个下标标记的结点内容
  89. void swap(int index1, int index2)
  90. {
  91. Node temp = new Node(tree[index1]);
  92. tree[index1] = new Node(tree[index2]);
  93. tree[index2] = temp;
  94. }
  95. //index1中结点的值是否比index2中结点的值小
  96. boolean compare(int index1, int index2)
  97. {
  98. if(tree[index1].height < tree[index2].height)
  99. return true;
  100. return false;
  101. }
  102. //往堆中加入一个元素,并调整堆
  103. void push(int x, int y, int height)
  104. {
  105. tree[len] = new Node(x, y, height);
  106. int current = len;
  107. int parent = (current - 1) / 2;
  108. //从下往上调整,小于父节点的上移
  109. while(current > 0)
  110. {
  111. if(compare(current, parent))
  112. {
  113. swap(current, parent);
  114. current = parent;
  115. parent = (current - 1) / 2;
  116. }
  117. else
  118. break;
  119. }
  120. len++;
  121. }
  122. //由于无法同时返回两个数值,取出堆中最小元素时,调用三个函数体
  123. int getX(int index)
  124. {
  125. return tree[index].x;
  126. }
  127. int getY(int index)
  128. {
  129. return tree[index].y;
  130. }
  131. //取出堆顶元素后,调整堆
  132. void pop()
  133. {
  134. //实现堆顶元素和堆底元素位置交换,并从堆中取出最小值
  135. len --;
  136. tree[0] = new Node(tree[len]);
  137. //从上到下调整堆
  138. int current = 0;
  139. int lChild = current * 2 + 1;
  140. int rChild = current * 2 + 2;
  141. while(current < len)
  142. {
  143. if(lChild >= len)
  144. break;
  145. //记录左右孩子中较小的结点
  146. int minChild = lChild;
  147. if(rChild<len && compare(rChild, minChild))
  148. minChild = rChild;
  149. //交换父子结点位置,把较大的结点挪到下面
  150. if(compare(minChild, current))
  151. {
  152. swap(minChild, current);
  153. current = minChild;
  154. lChild = current * 2 + 1;
  155. rChild = current * 2 + 2;
  156. }
  157. else
  158. break;
  159. }
  160. }
  161. }
  162. //结点类
  163. class Node
  164. {
  165. int x;
  166. int y;
  167. int height;
  168. Node() {}
  169. Node(Node node)
  170. {
  171. this.height = node.height;
  172. this.x = node.x;
  173. this.y = node.y;
  174. }
  175. Node(int x, int y, int height)
  176. {
  177. this.height = height;
  178. this.x = x;
  179. this.y = y;
  180. }
  181. }

Solution_4(大根堆+BFS,实现Solution_1)

遍历每个未被访问的点,数值小于等于该点的所有点接雨水后可以形成高度相同的水面,同一个水面的高度都是其四周方块最小的值,可以计算出每个水平面需要填补的雨水数量。
但如果数值不同时两个高度的水平面相邻,会影响水平面划分的结果,先使用堆对内部的点进行排序,从地势高的的开始划分同一水平面,可以避免影响水平面的划分。

  1. import java.util.Scanner;
  2. //结点类
  3. class Node
  4. {
  5. int x;
  6. int y;
  7. int height;
  8. Node() {}
  9. Node(Node node)
  10. {
  11. this.x = node.x;
  12. this.y = node.y;
  13. this.height = node.height;
  14. }
  15. Node(int x, int y, int height)
  16. {
  17. this.x = x;
  18. this.y = y;
  19. this.height = height;
  20. }
  21. }
  22. //大根堆
  23. class Heap
  24. {
  25. Node[] tree;
  26. int len;
  27. //构造函数
  28. Heap() {}
  29. //开辟空间
  30. Heap(int length)
  31. {
  32. tree = new Node[length];
  33. len = 0;
  34. }
  35. //打印堆中元素
  36. public void Print()
  37. {
  38. for(int i=0; i<len; i++)
  39. System.out.print(tree[i].x + " ");
  40. System.out.println();
  41. for(int i=0; i<len; i++)
  42. System.out.print(tree[i].y + " ");
  43. System.out.println();
  44. for(int i=0; i<len; i++)
  45. System.out.print(tree[i].height + " ");
  46. System.out.println();
  47. System.out.println();
  48. }
  49. //交换堆中两个下标标记的结点内容
  50. void swap(int index1, int index2)
  51. {
  52. Node temp = new Node(tree[index1]);
  53. tree[index1] = new Node(tree[index2]);
  54. tree[index2] = temp;
  55. }
  56. //index1中结点的值是否比index2中结点的值大
  57. boolean compare(int index1, int index2)
  58. {
  59. if(tree[index1].height > tree[index2].height)
  60. return true;
  61. return false;
  62. }
  63. //往堆中加入一个元素,并调整堆
  64. void push(int x, int y, int height)
  65. {
  66. tree[len] = new Node(x, y, height);
  67. int current = len;
  68. int parent = (current - 1) / 2;
  69. //从下往上调整,大于父节点的结点上移
  70. while(current > 0)
  71. {
  72. if(compare(current, parent))
  73. {
  74. swap(current, parent);
  75. current = parent;
  76. parent = (current - 1) / 2;
  77. }
  78. else
  79. break;
  80. }
  81. len++;
  82. }
  83. //由于无法同时返回两个数值,取出堆中最大元素时,需要调用三个函数体
  84. int getX(int index)
  85. {
  86. return tree[index].x;
  87. }
  88. int getY(int index)
  89. {
  90. return tree[index].y;
  91. }
  92. //取出堆顶元素后,调整堆
  93. void pop()
  94. {
  95. //实现堆顶元素和堆底元素位置交换,并从堆中取出最大值
  96. len--;
  97. tree[0] = new Node(tree[len]);
  98. //从上到下调整堆
  99. int current = 0;
  100. int lChild = current * 2 + 1;
  101. int rChild = current * 2 + 2;
  102. while(lChild < len)
  103. {
  104. //记录左右孩子中较小的结点
  105. int minChild = lChild;
  106. if(rChild<len && compare(rChild, minChild))
  107. minChild = rChild;
  108. //交换父子结点位置,把较小的结点挪到下面
  109. if(compare(minChild, current))
  110. {
  111. swap(minChild, current);
  112. current = minChild;
  113. lChild = current * 2 + 1;
  114. rChild = current * 2 + 2;
  115. }
  116. else
  117. break;
  118. }
  119. }
  120. }
  121. public class rain
  122. {
  123. static int T;
  124. static int Row, Col;
  125. static int[][] map;
  126. //所有可能存储雨水的格子按高度降序排序
  127. static Heap heap;
  128. //已经接了雨水的格子被标为true
  129. static boolean[][] vis_raw;
  130. static int[][] dir = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
  131. static int ans;
  132. public static void main(String[] args)
  133. {
  134. Scanner sc = new Scanner(System.in);
  135. T = sc.nextInt();
  136. for(int t=0; t<T; t++)
  137. {
  138. Row = sc.nextInt();
  139. Col = sc.nextInt();
  140. map = new int[Row][Col];
  141. heap = new Heap(Row * Col);
  142. vis_raw = new boolean[Row][Col];
  143. ans = 0;
  144. for(int i=0; i<Row; i++)
  145. {
  146. for(int j=0; j<Col; j++)
  147. {
  148. map[i][j] = sc.nextInt();
  149. if (i==0 || i==Row-1 || j==0 || j==Col-1)
  150. continue;
  151. //除了边界外的格子全都进入堆
  152. heap.push(i, j, map[i][j]);
  153. }
  154. }
  155. // heap.Print();
  156. bfs();
  157. System.out.println(ans);
  158. }
  159. sc.close();
  160. }
  161. private static void bfs()
  162. {
  163. while(heap.len > 0)
  164. {
  165. //由高到低取出堆中还没有存雨水的点
  166. int row = heap.getX(0);
  167. int col = heap.getY(0);
  168. heap.pop();
  169. if(vis_raw[row][col])
  170. continue;
  171. //水面四周最矮的边界
  172. int min = Integer.MAX_VALUE;
  173. int count = 0, sum = 0; //接雨水的格子个数、底部总共的格子数,用于计算该水面区域接到的雨水
  174. //拷贝接雨水的区域,如果发现边界太低无法接水,可以免于回溯
  175. boolean[][] vis = new boolean[Row][Col];
  176. for(int i=1; i<Row-1; i++)
  177. for(int j=0; j<Col-1; j++)
  178. vis[i][j] = vis_raw[i][j];
  179. //bfs核心:将可以归为同一水平面的点入队,并进行宽搜
  180. int[][] queue = new int[Row * Col][2];
  181. int head = 0, tail = 0;
  182. queue[tail][0] = row;
  183. queue[tail][1] = col;
  184. tail++;
  185. vis[row][col] = true;
  186. count++;
  187. int cur_height = map[row][col];
  188. sum += cur_height;
  189. //如果一个平面到了四周,高度还是低于当前点的高度,说明接不住雨水
  190. boolean leak = false;
  191. while(head<tail && !leak)
  192. {
  193. for(int i=0; i<dir.length; i++)
  194. {
  195. int next_row = queue[head][0] + dir[i][0];
  196. int next_col = queue[head][1] + dir[i][1];
  197. //因为加入队列中的点都不是边界,所以不会有数组越界的情况,无需判断
  198. int next_height = map[next_row][next_col];
  199. //该点四周的点地势比当前点更低,存在两种情况:
  200. //1. 四周的点位于边缘,该区域无法接水,会漏
  201. //2. 四周的点不在边缘,也是同一水平面的点,加入队列
  202. if(next_height <= cur_height)
  203. {
  204. if(next_row==0 || next_row==Row-1 || next_col==0 || next_col==Col-1)
  205. {
  206. leak = true;
  207. break;
  208. }
  209. if(!vis[next_row][next_col])
  210. {
  211. queue[tail][0] = next_row;
  212. queue[tail][1] = next_col;
  213. tail++;
  214. vis[next_row][next_col] = true;
  215. count++;
  216. sum += next_height;
  217. }
  218. }
  219. //该点四周的点地势比当前点更高,判断是否可以更新边界的最小值
  220. else if(next_height < min)
  221. min = next_height;
  222. }
  223. head++;
  224. }
  225. //可以接水
  226. if(!leak)
  227. {
  228. //把当前已经接雨水的点更新到全局
  229. for(int i=1; i<Row-1; i++)
  230. for(int j=1; j<Col-1; j++)
  231. vis_raw[i][j] = vis[i][j];
  232. //计算当前水平面接到的雨水,更新到结果
  233. int rain = min * count - sum;
  234. ans += rain;
  235. }
  236. }
  237. }
  238. }