接雨水问题
题目描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
提示:
- n = = h e i g h t . l e n g t h n == height.length n==height.length
- 0 < = n < = 3 ∗ 104 0 <= n <= 3 * 104 0<=n<=3∗104
- 0 < = h e i g h t [ i ] < = 105 0 <= height[i] <= 105 0<=height[i]<=105
如图所示,对于某一个具体的位置 h e i g h t [ i ] height[i] height[i]来说,给定的柱子宽度都是1,因此,该位置可能接的雨水量就只和高度相关。根据木桶盛水的短板原则, h e i g h t [ i ] height[i] height[i]位置能接多少水,还取决于它两边的情况。即左边的最大高度 m a x _ l e f t max\_left max_left和右边的最大高度 m a x _ r i g h t max\_right max_right,并且最多接水量只和两者中最小的高度 m i n ( m a x _ l e f t , m a x _ r i g h t ) min(max\_left, max\_right) min(max_left,max_right)相关。
- 如果 m i n ( m a x _ l e f t , m a x _ r i g h t ) > h e i g h t [ i ] min(max\_left, max\_right) > height[i] min(max_left,max_right)>height[i],那么 h e i g h t [ i ] height[i] height[i]位置接水量就是两者的差值
- 如果 min ( m a x _ l e f t , m a x _ r i g h t ) < h e i g h t [ i ] \min(max\_left, max\_right) < height[i] min(max_left,max_right)<height[i],那么 h e i g h t [ i ] height[i] height[i]位置无法接水
如何理解呢?以题目中给的🌰为例:
-
如果当前位置为 h e i g h t [ 1 ] height[1] height[1],那么情况如下所示:
左边最高为0,右边最高为3,当前位置为1。根据上述的原则,该位置无法接水
-
如果当前位置为 h e i g h t [ 3 ] height[3] height[3],情况如下所示:
左边最高为1,右边最高为3,当前位置为1,那么该位置也无法接水
-
如果当前位置为 h e i g h t [ 7 ] height[7] height[7],情况如下所示:
左边最高为2,右边最高为3,当前位置为1。满足上述能接水的原则,因此,该位置接水量为
min ( m a x _ l e f t , m a x _ r i g h t ) − h e i g h t [ 7 ] = min ( 2 , 3 ) − 1 = 1 \min(max\_left,max\_right) - height[7]=\min(2,3)-1 = 1 min(max_left,max_right)−height[7]=min(2,3)−1=1
所以,问题的关键就在于如何找到某一位置 h e i g h t [ i ] height[i] height[i]的左最高 m a x _ l e f t max\_left max_left和右最高 m a x _ r i g h t max\_right max_right。
首先,我们来看最直观的方法,即到达一个具体的位置先找 m a x _ l e f t max\_left max_left和 m a x _ r i g h t max\_right max_right。假设当前位置为 h e i g h t [ i ] height[i] height[i],那么 m a x _ l e f t max\_left max_left只能在它的左半部分找且不包含自身,即:
int max_left = 0;
for(int j = i - 1; j >= 0; j--){
if(height[j] > max_left){
max_left = height[j];
}
}
对应的 m a x _ r i g h t max\_right max_right只能在它的右半部分找且不含自身,即:
int max_right = 0;
for(int j = i + 1; j < height.length; j++){
if(height[j] > max_right){
max_right = height[j];
}
}
当找到了 m a x _ l e f t max\_left max_left和 m a x _ r i g h t max\_right max_right,按照前面的分析,自然就可以得到该位置的接水量。由于两端不可能接雨水,因此,讨论的范围为 [ 1 , l e n ( h e i g h t ) − 1 ) [1, len(height) - 1) [1,len(height)−1)。下面逐步的通过图示的方式理解下上述的流程。
-
h i e g h t [ 1 ] hieght[1] hieght[1]:当前位置为1, m a x _ l e f t = 0 max\_left=0 max_left=0, m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为0
-
h i e g h t [ 2 ] hieght[2] hieght[2]:当前位置为0, m a x _ l e f t = 1 max\_left=1 max_left=1, m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为1
-
h i e g h t [ 3 ] hieght[3] hieght[3]:当前位置为12, m a x _ l e f t = 1 max\_left=1 max_left=1, m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为0
-
h i e g h t [ 4 ] hieght[4] hieght[4]:当前位置为2, m a x _ l e f t = 1 max\_left=1 max_left=1, m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为0
-
h i e g h t [ 5 ] hieght[5] hieght[5]:当前位置为1, m a x _ l e f t = 2 max\_left=2 max_left=2, m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为1
-
h i e g h t [ 6 ] hieght[6] hieght[6]:当前位置为0, m a x _ l e f t = 2 max\_left=2 max_left=2, m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为2
-
h i e g h t [ 7 ] hieght[7] hieght[7]:当前位置为1, m a x _ l e f t = 2 max\_left=2 max_left=2, m a x _ r i g h t = 3 max\_right=3 max_right=3,接水量为1
-
h i e g h t [ 10 ] hieght[10] hieght[10]:当前位置为1, m a x _ l e f t = 3 max\_left=3 max_left=3, m a x _ r i g h t = 2 max\_right=2 max_right=2,接水量为0
-
h i e g h t [ 11 ] hieght[11] hieght[11]:当前位置为2, m a x _ l e f t = 3 max\_left=3 max_left=3, m a x _ r i g h t = 1 max\_right=1 max_right=1,接水量为0
通过上述的逐步分析,最终的接水量为6。详细的解题代码如下所示:
/**
* @Author dyliang
* @Date 2020/11/10 22:39
* @Version 1.0
*/
public class _42 {
public static void main(String[] args) {
int[] height = {0,1,0,2,1,0,1,3,2,1,2,1};
System.out.println(trap4(height));
}
public static int trap2(int[] height){
int sum = 0;
for (int i = 1; i < height.length - 1; i++) {
int max_left = 0;
for(int j = i - 1; j >= 0; j--){
if(height[j] > max_left){
max_left = height[j];
}
}
int max_right = 0;
for(int j = i + 1; j < height.length; j++){
if(height[j] > max_right){
max_right = height[j];
}
}
int min_one = Math.min(max_left,max_right);
sum += Math.max(0, min_one - height[i]);
}
return sum;
}
}
仔细分析,这种解法需要每个位置都遍历一次给定的数组找 m a x _ l e f t max\_left max_left和 m a x _ r i g h t max\_right max_right,时间复杂度为 O ( N 2 ) O(N^2) O(N2)。由于过程中没有使用额外的存储空间,空间复杂度为 O ( 1 ) O(1) O(1)。
如果详细的走过每一个位置可以发现, m a x _ l e f t max\_left max_left和 m a x _ r i g h t max\_right max_right在很多位置上并没有发生改变,如下图黄框所示的位置,每一个框内所有位置的的 m a x _ l e f t max\_left max_left和 m a x _ r i g h t max\_right max_right都是相同的。如果我们提前将每个位置的 m a x _ l e f t max\_left max_left和 m a x _ r i g h t max\_right max_right找到并存储起来,那么求接水量的时间复杂度就会下降到 O ( N ) O(N) O(N)。虽然,此时空间复杂度变成了 O ( N ) O(N) O(N),但是空间换时间是完全可接受的。
那么如何找每个位置的 m a x _ l e f t max\_left max_left和 m a x _ r i g h t max\_right max_right呢?假设从左往右找 m a x _ l e f t max\_left max_left,当前遍历到了 h e i g h t [ i ] height[i] height[i],那么它对应的 m a x _ l e f t [ i ] max\_left[i] max_left[i]取决于 h e i g h t [ i − 1 ] height[i-1] height[i−1]和 h e i g h t [ i − 1 ] height[i-1] height[i−1]位置对应的 m a x _ l e f t [ i − 1 ] max\_left[i-1] max_left[i−1],而且 m a x _ l e f t [ i ] max\_left[i] max_left[i]的值是两者的最大值。
而 m a x _ r i g h t max\_right max_right数组的值取决于每个位置右半部分的情况,因此需要从右往左遍历,对应的原则是 m a x _ r i g h t [ i ] = max ( m a x _ r i g h t [ i + 1 ] , h e i g h t [ i + 1 ] ) max\_right[i] = \max(max\_right[i + 1], height[i + 1]) max_right[i]=max(max_right[i+1],height[i+1])。
详细的解题代码如下:
/**
* @Author dyliang
* @Date 2020/11/10 22:39
* @Version 1.0
*/
public class _42 {
public static void main(String[] args) {
int[] height = {0,1,0,2,1,0,1,3,2,1,2,1};
System.out.println(trap4(height));
}
public static int trap3(int[] height){
int sum = 0;
int h = height.length;
int[] max_left = new int[h];
int[] max_right = new int[h];
for (int i = 1; i < h - 1; i++) {
max_left[i] = Math.max(max_left[i - 1], height[i - 1]);
}
for (int i = h - 2; i >= 0; i--) {
max_right[i] = Math.max(max_right[i + 1], height[i + 1]);
}
for (int i = 1; i < h - 1; i++) {
int min_one = Math.min(max_left[i], max_right[i]);
sum += Math.max(0, min_one - height[i]);
}
return sum;
}
}
上面基于备忘录的方法的时间复杂度和空间复杂度都是 O ( N ) O(N) O(N),由于总是至少要遍历一遍数组,因此,时间复杂度最低也只能是 O ( N ) O(N) O(N),那么能否不用额外的存储空间完成呢?
仔细分析上面的解法可以发现,对于 h e i g h t [ i ] height[i] height[i]来说,它只关心它对应的 m a x _ l e f t max\_left max_left,而不关心曾经出现过的 m a x _ l e f t max\_left max_left。同样对于 m a x _ r i g h t max\_right max_right来说,也只关心它对应的 m a x _ r i g h t max\_right max_right,而不关心曾经的 m a x _ r i g h t max\_right max_right。因此,我们可以使用两个变量来记录目前最大的 m a x _ l e f t max\_left max_left和 m a x _ r i g h t max\_right max_right。而两端位置的遍历,可以使用指针 l e f t left left和 r i g h t right right进行记录。
而且对于 l e f t left left来说,它对应的 m a x _ l e f t max\_left max_left是不会在变化的,而此时的 m a x _ r i g h t max\_right max_right却不一定。因为, l e f t left left到 m a x _ r i g h t max\_right max_right对应索引位置之间可能存在更大的值。同样,对于 r i g h t right right来说, m a x _ r i g h t max\_right max_right是不会变化的,但是 m a x _ l e f t max\_left max_left却可能变化。但是,只要 m a x _ l e f t < m a x _ r i g h t max\_left < max\_right max_left<max_right成立,即时出现更大的 m a x _ r i g h t max\_right max_right,根据接水的原则可知,它不会影响当前的结果。同理,只要 m a x _ r i g h t > m a x _ l e f t max\_right > max\_left max_right>max_left成立,即时出现更大的 m a x _ l e f t max\_left max_left,同样不会影响当前的结果。
详细的解题代码如下:
/**
* @Author dyliang
* @Date 2020/11/10 22:39
* @Version 1.0
*/
public class _42 {
public static void main(String[] args) {
int[] height = {0,1,0,2,1,0,1,3,2,1,2,1};
System.out.println(trap4(height));
}
public static int trap(int[] height) {
int sum = 0;
int left = 0, right = height.length - 1;
int left_max = 0, right_max = 0;
while(left <= right){
// 处理left所指的位置
if(left_max < right_max){
// 记录接水量
sum += Math.max(0, left_max - height[left]);
// 更新可能的max_left
left_max = Math.max(left_max, height[left]);
left++;
// 处理right所指的位置
} else {
// 记录接水量
sum += Math.max(0, right_max - height[right]);
// 更新可能的max_right
right_max = Math.max(right_max, height[right]);
right--;
}
}
return sum;
}
}