今天总结一下 LeetCode 上的几道跟接雨水有关的题目。
题目链接:11. Container With Most Water
原题如下:
Given n non-negative integers a 1 , a 2 , . . . , a n , a_1, a_2,...,a_n, a1,a2,...,an, where each represents a point at coordinate (i, a i a_i ai). n vertical lines are drawn such that the two endpoints of line i is at (i, a i a_i ai) and (i, 0). Find two lines, which together with x-axis forms a container, such that the container contains the most water.
Note: You may not slant the container and n is at least 2.
题意大概是:
给你 n 个非负整数 a 1 , a 2 , . . . , a n , a_1, a_2,...,a_n, a1,a2,...,an, 每一个数都代表坐标系上的一个点 (i, a i a_i ai)。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, a i a_i ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
注意:你不能倾斜容器,且 n 的值至少为 2。
比如下图中的垂直线可以用数组 [1,8,6,2,5,4,8,3,7] 表示,在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
首先想到的就是这道题可以暴力求解,首先从左往右遍历数组中的每一个元素,当遍历到第 i ( 1 ≤ i < n ) i(1\le i< n) i(1≤i<n) 个元素时,我们依次考察 a i a_i ai 与 a j ( i < j ≤ n ) a_j(i
虽然时间复杂度会很高,但是实现一下也无妨,至少可以用来验证其他算法执行得是否正确。
代码如下:
public int maxArea(int[] height) {
int maxArea = Integer.MIN_VALUE;
for (int i = 0; i < height.length - 1; i++) {
for (int j = i + 1; j < height.length; j++) {
int area = (j - i) * Math.min(height[j], height[i]);
if (maxArea < area){
maxArea = area;
}
}
}
return maxArea;
}
复杂度分析
我们知道,线与线之间形成的面积总是受到较短的线的高度的限制。两条线相距越远,得到的面积就越大。我们设置两个指针,一个指向数组的开头,另一个指向数组的末尾,再维护一个变量 maxarea,记录目前为止遇到的最大面积,每次计算完两个指针指向的直线构成的面积之后,我们让指向更短的那条直线的指针向前一步,直到两个指针相遇。
LeetCode第11题算法示例
代码如下:
public int maxArea(int[] height){
int maxArea = Integer.MIN_VALUE;
for (int i = 0, j = height.length - 1; i < j;) {
int hi = height[i], hj = height[j];
int area;
if (hi >= hj){
area = (j - i) * hj;
j--;
}else {
area = (j - i) * hi;
i++;
}
if (area > maxArea){
maxArea = area;
}
}
return maxArea;
}
为什么这种方法是有效的呢?一开始,假设两个水的面积由最外侧的两条直线构成,为了使面积最大,我们需要找到更长的直线。如果我们移动指向较长的那条直线的指针,水的面积不会增大,因为面积取决于较短的那条直线。但是如果移动指向较短的那条直线的指针,尽管两个指针之间的距离缩短了,但是仍然有利于我们找到最大的面积。这样做是因为通过移动短指针获得的相对较长的直线可能会抵消宽度减少所导致的面积减少。
我们也可以用反证法来证明这个算法是有效的。
假设我们得到的结果不是最优解,那么肯定存在一条直线 a_ol 和 a_or 构成最优解。因为只有当两个指针相遇时我们的算法才停止,所以我们一定会遇到 a_ol 和 a_or 中的至少一个,不失一般性,假设我们遇到了 a_ol 没有遇到 a_or,当指针停在 a_ol 上时,不会再往前移动,除非遇到下面两种情况:
所以该算法是有效的。
复杂度分析
题目链接:42. 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.
题意大概是:
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
比如下图是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
我们将柱子的个数记为 n,第 i 根柱子的高度记为 h i h_i hi,第 i 根柱子上能接的雨水记为 w i w_i wi,第 i 根柱子左侧最高的柱子的高度记为 m a x l e f t i = m a x { h 1 , h 2 , ⋯ , h i } maxleft_i=max\{h_1,h_2,\cdots,h_i\} maxlefti=max{h1,h2,⋯,hi},第 i 根柱子右侧最高的柱子的高度记为 m a x r i g h t i = m a x { h i , h i + 1 , ⋯ , h n } maxright_i=max\{h_i, h_{i+1},\cdots,h_n\} maxrighti=max{hi,hi+1,⋯,hn},那么 w i = m i n { m a x l e f t i , m a x r i g h t i } − h i w_i=min\{maxleft_i,maxright_i\}-h_i wi=min{maxlefti,maxrighti}−hi,将每根柱子能够接的雨水加起来就是我们最后想要的结果了。若结果为 res,那么
r e s = ∑ i = 1 n w i res=\sum^n_{i=1}w_i res=i=1∑nwi
根据这个思路,可以写出如下代码:
public int trap(int[] height) {
if (height == null || height.length == 0)
return 0;
int res = 0, maxLeft = height[0];
for (int i = 1; i < height.length - 1; i++) {
maxLeft = Math.max(maxLeft, height[i]);
int maxRight = height[i];
for (int j = i + 1; j < height.length; j++) {
maxRight = Math.max(maxRight, height[j]);
}
res += Math.min(maxLeft, maxRight) - height[i];
}
return res;
}
复杂度分析
根据思路一, w i = m i n { m a x l e f t i , m a x r i g h t i } − h i w_i=min\{maxleft_i,maxright_i\}-h_i wi=min{maxlefti,maxrighti}−hi,所以每次都要遍历 h i h_i hi 的左右两侧,找到 m a x l e f t i maxleft_i maxlefti 和 m a x r i g h t i maxright_i maxrighti。那我们何不先把 m a x l e f t i maxleft_i maxlefti 和 m a x r i g h t i maxright_i maxrighti 记录下来呢,当需要用到的时候直接取就可以了。也就是说,在计算能够接多少雨水之前,我们先生成两个数组,maxleft 和 maxright,其中 m a x l e f t i maxleft_i maxlefti 是第 i 根柱子左侧最高的柱子的高度, m a x r i g h t i maxright_i maxrighti 是第 i 根柱子右侧最高的柱子的高度,最后再遍历一遍柱子的数组,算出 ∑ i = 1 n ( m i n { m a x l e f t i , m a x r i g h t i } − h i ) \sum^n_{i=1}(min\{maxleft_i,maxright_i\}-h_i) ∑i=1n(min{maxlefti,maxrighti}−hi) 就可以了。
代码如下:
public int trap_dp(int[] height) {
if (height == null || height.length == 0)
return 0;
int res = 0, len = height.length;
int[] left_max = new int[len];
int[] right_max = new int[len];
left_max[0] = height[0];
for (int i = 1; i < len; i++) {
left_max[i] = Math.max(left_max[i - 1], height[i]);
}
right_max[len - 1] = height[len - 1];
for (int i = len - 2; i >= 0; i--) {
right_max[i] = Math.max(right_max[i + 1], height[i]);
}
for (int i = 0; i < len; i++) {
res += Math.min(left_max[i], right_max[i]) - height[i];
}
return res;
}
复杂度分析
我们知道,一根柱子,只有它两侧都有比它高的柱子的时候,才能够存下水。首先我们让第一根柱子的索引入栈,即让 1 入栈,接下来从第 2 根柱子开始,我们将当前遍历到的柱子高度记为 h i h_i hi,栈顶记为 top,因为栈中存的是索引,所以栈顶柱子的高度就是 h t o p h_{top} htop,根据以下几种情况,做相应的操作:
有了上述思路,就可以写出代码了。
public int trap_stack2(int[] height){
if (height == null || height.length == 0)
return 0;
int res = 0;
Stack<Integer> stack = new Stack<>();
stack.push(0);
for (int i = 1; i < height.length; i++) {
while (stack.size() >= 2 && height[i] > height[stack.peek()]) {
int top = stack.pop();
int leftBoundIdx = stack.peek();
int leftBound = height[leftBoundIdx], distance = i - leftBoundIdx - 1;
res += (Math.min(leftBound, height[i]) - height[top]) * distance;
}
while (!stack.isEmpty() && height[i] > height[stack.peek()]){
stack.pop();
}
stack.push(i);
}
return res;
}
复杂度分析
我们用两个指针 i 和 j,刚开始他们分别指向第一个柱子和最后一个柱子,两个指针相向而行,在前进的过程中,维护两个变量: maxleft 和 maxright,分别表示 h i h_i hi 左边最高的柱子的高度和 h j h_j hj 右边最高的柱子的高度。
当 i 如果 h i > m a x l e f t h_i>maxleft hi>maxleft,则将 maxleft 的值更新为 h i h_i hi。 如果 h j > m a x r i g h t h_j>maxright hj>maxright,则将 maxright 的值更新为 h j h_j hj。 如果 maxleft < maxright,计算 h i h_i hi 能存的雨水 w i = m a x l e f t − h i w_i=maxleft-h_i wi=maxleft−hi,i 前进一步。 否则计算 h j h_j hj 能存的雨水 w j = m a x r i g h t − h j w_j=maxright-h_j wj=maxright−hj,j 前进一步。 将每次计算得到的雨水加起来就是最终的结果了。以下是代码实现: 简单解释一下这个思路为什么有效。上述代码中的 maxleft 相当于我们之前说的 m a x l e f t i maxleft_i maxlefti ,maxright 相当于 m a x r i g h t j maxright_j maxrightj 我们知道 w i w_i wi 取决于 m i n { m a x l e f t i , m a x r i g h t i } min\{maxleft_i,maxright_i\} min{maxlefti,maxrighti},由于 i 复杂度分析 题目链接:407. Trapping Rain Water II 原题如下: Given an Note: Both m and n are less than 110. The height of each unit cell is greater than 0 and is less than 20,000. 题意大概是: 给定一个 m × n m\times n m×n 的矩阵,其中的值均为正整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。 注意:m 和 n 都是小于 110 的整数。每一个单位的高度都大于 0 且小于 20000。 如下图所示,这是下雨前的状态,其高度图可以用这个数组表示: 首先我们把地图最外层的那一圈柱子逐个放入优先级队列(小顶堆)queue,作为水池的「外壁」,然后从 queue 中取最矮的那根柱子 min,同时记录已经出队的柱子中最高的高度 maxbound,maxbound 就是当前水池能够存的最大水位。我们分别考察 min 的上下左右相邻的柱子 min_neibor 是否比 maxbound 矮,如果是话,说明可以存水,那我们的存水量就加上「min_neibor - maxbound」。 代码如下: 复杂度分析
public int trap_TwoPoints(int[] height){
if (height == null || height.length == 0)
return 0;
int i = 0, j = height.length - 1;
int maxLeft = 0, maxRight = 0, res = 0;
while (i < j){
if (height[i] > maxLeft){
maxLeft = height[i];
}
if (height[j] > maxRight){
maxRight = height[j];
}
if (maxLeft < maxRight){
res += maxLeft - height[i];
i++;
}else {
res += maxRight - height[j];
j--;
}
}
return res;
}
第 407 题 第 42 题升级版——三维地图接雨水
m x n
matrix of positive integers representing the height of each unit cell in a 2D elevation map, compute the volume of water it is able to trap after raining.
{
{1,4,3,1,3,2},
{3,2,1,3,2,4},
{2,3,3,2,3,1}
}
解题思路
class Solution {
//地图上的柱子
private static class Bar implements Comparable<Bar>{
int i; //柱子所在的行
int j; //柱子所在的列
int height; //柱子的高度
Bar(int i, int j, int height){
this.i = i;
this.j = j;
this.height = height;
}
@Override
public int compareTo(Bar o) {
return this.height - o.height;
}
}
public int trapRainWater(int[][] heightMap) {
if (heightMap == null || heightMap.length < 3 || heightMap[0].length < 3){
return 0;
}
int m = heightMap.length, n = heightMap[0].length;
boolean[][] visited = new boolean[m][n];
Queue<Bar> pq = new PriorityQueue<>();
for (int i = 0; i < m; i++) {
Bar bar1 = new Bar(i, 0, heightMap[i][0]);
pq.add(bar1);
visited[i][0] = true;
Bar bar2 = new Bar(i, n - 1, heightMap[i][n - 1]);
pq.add(bar2);
visited[i][n - 1] = true;
}
for (int i = 1; i < n - 1; i++) {
Bar bar1 = new Bar(0, i, heightMap[0][i]);
pq.add(bar1);
visited[0][i] = true;
Bar bar2 = new Bar(m - 1, i, heightMap[m - 1][i]);
pq.add(bar2);
visited[m - 1][i] = true;
}
int res = 0;
int maxBound = Integer.MIN_VALUE; //相当于二维空间里面的maxLeft,即能够拦截住的最大水位
while (!pq.isEmpty()){
Bar bar = pq.poll();
maxBound = Math.max(maxBound, bar.height);
Bar temp;
if (bar.i - 1 >= 0 && !visited[bar.i - 1][bar.j]){
temp = new Bar(bar.i - 1, bar.j, heightMap[bar.i - 1][bar.j]);
pq.add(temp);
visited[bar.i - 1][bar.j] = true;
if (maxBound > temp.height){
res += maxBound - temp.height;
}
}
if (bar.i + 1 < m && !visited[bar.i + 1][bar.j]){
temp = new Bar(bar.i + 1, bar.j, heightMap[bar.i + 1][bar.j]);
pq.add(temp);
visited[bar.i + 1][bar.j] = true;
if (maxBound > temp.height){
res += maxBound - temp.height;
}
}
if (bar.j - 1 >= 0 && !visited[bar.i][bar.j - 1]){
temp = new Bar(bar.i, bar.j - 1, heightMap[bar.i][bar.j - 1]);
pq.add(temp);
visited[bar.i][bar.j - 1] = true;
if (maxBound > temp.height){
res += maxBound - temp.height;
}
}
if (bar.j + 1 < n && !visited[bar.i][bar.j + 1]){
temp = new Bar(bar.i, bar.j + 1, heightMap[bar.i][bar.j + 1]);
pq.add(temp);
visited[bar.i][bar.j + 1] = true;
if (maxBound > temp.height){
res += maxBound - temp.height;
}
}
}
return res;
}
}