[LeetCode]Trapping Rain Water - EpoTalk
Trapping Rain Water
Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it is able to trap after raining.
For example,
Given[0,1,0,2,1,0,1,3,2,1,2,1]
, return6
.
分析
对于某一点来说,先分别找出左右最大高度,取较小值,如果本身高度低于较小值的话,可以蓄水。
找出左右最大高度的方法很简单,对数组分别从左扫一遍,从右再扫一遍即可。
当然也有space为O(1)的方法,方法很巧妙,反向思维,我们从边界即两端出发。维护两个变量leftMax
, rightMax
, 初始值为两端边界高度。然后用两个pointer,分别从左与从右。每次我们取leftMax
, rightMax
中较小的一个, 然后取相应方向的pointer表示的点,直接可以算出来那个点的存水量。然后根据那个点高度更新leftMax
或rightMax
值。可以这样算的原因是每次取leftMax
, rightMax
中较小的一个, 可以保证该值就是最往左及往右方向的较小值。
这道题也有Follow up,即是Trapping Rain Water II, 是LintCode的一道题,具体见下文。
用上文提到的最后一种方法类似思想,可解决这题。
复杂度
time: O(n), space: O(n)
代码
public class Solution { public int trap(int[] height) { int[] leftMax = new int[height.length]; int res = 0; // 求出各点左边最大值 for (int i = 1; i < height.length; i++) {leftMax[i] = Math.max(leftMax[i - 1], height[i - 1]); } int rightMax = 0; for (int i = height.length - 1; i >= 0; i--) {res += Math.max(0, Math.min(leftMax[i], rightMax) - height[i]);rightMax = Math.max(rightMax, height[i]); // 更新当前点之后右边最大值 } return res; }}// O(1) spacepublic class Solution { public int trap(int[] height) { if (height == null || height.length == 0)return 0; int leftMax = height[0]; int rightMax = height[height.length - 1]; int res = 0; int i = 1; int j = height.length - 2; while (i <= j) {if (leftMax < rightMax) { res += Math.max(0, leftMax - height[i]); leftMax = Math.max(leftMax, height[i]); i++;} else { res += Math.max(0, rightMax - height[j]); rightMax = Math.max(rightMax, height[j]); j--;} } return res; }}
Trapping Rain Water II
Given n x m non-negative integers representing an elevation map 2d where the area of each cell is 1 x 1, compute how much water it is able to trap after raining.
Example:
Given 5*4 matrix[12, 13, 0, 12][13, 4, 13, 12][13, 8, 10, 12][12, 13, 12, 12][13, 13, 13, 13]return 14.
分析
这道题的思想跟上题是一样的,只不过这道题是立体的,上道题是平面的。同样对于某一个点,我们需要找到其周边最小高度,然后如果本身高度低于最小高度,即可蓄水。
关键问题是怎么找到周边最小高度,注意这里周边最小高度不仅仅只是往上往下往左往右的四个最大高度里取最小那么简单,这也是这道题我开始做错的地方。比如题目中(1,1)
这个cell,四个方向最大高度都为13
,但蓄水量并不是9
,而是8
, 因为水会沿着4->8->10->12
流。
周边最小高度应该指的是以这个点的邻居为中心开始向外扩展,不断扩展直至到周围四个边,然后得到不同方向扩展过程中的最大高度,取这些高度的最小值即可。
如果正向考虑的话无论是DP还是DFS都很麻烦。DP由于两个点之间的都互相有关系,无法找到推导式。DFS更是麻烦,先要四个方向扩展过程中最大值,再得出最小值,没写出比较简单的实现方法。
最后可行的一种方法是用Priority Queue,反向考虑,先把整个矩阵周围四个边的点全部push到queue里。然后pop出高度最小的点, 对于那个点的上下左右邻居来说,他们的周边最小高度必然是pop出的点的高度。如果邻居高度小于那个pop出点高度,水可以注入邻居,可以把邻居push到queue中。如果邻居高度大于pop出点高度,意味着水无法注入邻居,把邻居push到queue中同时也要更新邻居本身高度。所以说,在queue中bar的height表示的并不是那个点的实际高度,而是周围四个边扩散进来的高度。
用这个方法,同样可以解决二维的题目,具体上文代码实现。
复杂度
time: O(mn*logmn), space: O(mn)
代码
public class Solution { public class Bar{ int x; int y; int height; public Bar(int x, int y, int height) {this.x = x;this.y = y;this.height = height; } } public int trap(int[][] height) { int rows = height.length; if (rows == 0)return 0; int cols = height[0].length; int res = 0; PriorityQueue<Bar> pq = new PriorityQueue<>(new Comparator<Bar>() {public int compare(Bar b1, Bar b2) { return b1.height - b2.height;} }); boolean[][] visited = new boolean[rows][cols]; // 把周围四个边上的cell push到queue里 for (int j = 0; j < cols; j++) {pq.add(new Bar(0, j, height[0][j]));pq.add(new Bar(rows - 1, j, height[rows - 1][j]));visited[0][j] = true;visited[rows - 1][j] = true; } for (int i = 2; i < rows - 1; i++) {pq.add(new Bar(i, 0, height[i][0]));pq.add(new Bar(i, cols - 1, height[i][0]));visited[i][0] = true;visited[i][cols - 1] = true; } int[][] dirs = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; while (!pq.isEmpty()) {Bar bar = pq.remove();for (int k = 0; k < dirs.length; k++) { int x = bar.x + dirs[k][0]; int y = bar.y + dirs[k][1]; if (x >= 0 && x < rows && y >= 0 && y < cols && !visited[x][y]) { visited[x][y] = true; res += Math.max(0, bar.height - height[x][y]); // 水量必然知道,因为周边最小值已知 // 只有自己高度大于bar高度时,才用自己高度,否则仍然继承bar高度 Bar newBar = new Bar(x, y, bar.height < height[x][y] ? height[x][y] : bar.height); pq.add(newBar); }} } return res; }}