盛最多水的容器
题目描述:给你 n n n 个非负整数 a 1 , a 2 , . . . , a n a_1, a_2, ..., a_n a1,a2,...,an 每个数代表坐标中的一个点 ( i , a i ) (i, a_i) (i,ai) 。在坐标内画 n n n 条垂直线,垂直线 i i i 的两个端点分别为 ( i , a i ) (i, a_i) (i,ai) 和 ( i , 0 ) (i, 0) (i,0) 。找出其中两条线,使得它们与 x x x 轴共同构成的容器可以容纳最多的水。
要求:你不能倾斜容器。
示例:
输入: [ 1 , 8 , 6 , 2 , 5 , 4 , 8 , 3 , 7 ] [1, 8, 6, 2, 5, 4, 8, 3, 7] [1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [ 1 , 8 , 6 , 2 , 5 , 4 , 8 , 3 , 7 ] [1, 8, 6, 2, 5, 4, 8, 3, 7] [1,8,6,2,5,4,8,3,7] ,在此情况下,容器能够容纳水的最大值为49(图中蓝色部分所示)。
题目解析:找到一组左右边界使得蓝色部分面积最大
蓝色部分的面积为:
左右边界之间的垂直距离 * 数值较小的边界高度
放入数组理解就是求解:
m a x { ( j − i ) ∗ m i n ( a r r [ i ] , a r r [ j ] ) } \color{blue}max\{\ (j-i)\ *\ min(arr[i],\ arr[j])\ \} max{ (j−i) ∗ min(arr[i], arr[j]) }
解题思路:
最简单的想法自然是暴力,两重for循环遍历所有的边界情况,记录最大面积即可。
代码实现:
class Solution {
public int maxArea(int[] height) {
int len = height.length;
int water=0; // 最大盛水量
for(int i=0;i<len-1;i++){
for(int j=i+1;j<len;j++){
water = Math.max(water, (j-i)*Math.min(height[i], height[j]));
}
}
return water;
}
}
纯暴力超时,哈哈,不过还是通过了55个样例(有些比赛可以混很多分了),后面的样例可太长了,用 O ( n 2 ) O(n^2) O(n2) 的复杂度跑的话肯定是超时的。
如何降低时间复杂度?还需要从重新理解题目入手。
决定蓝色部分面积的其实就是长和宽,宽取决于两边界间的距离,所以相同情况下两边界间的距离是越远越好;长取决于较短边界的长度,也就与较长边界的长度无关了,无论长度多长,较短边的长度不够也没得办法,就像一个由参差不齐的木板组成的水桶一样,最终能盛多少水,还是得看最短的那一块木板多长的。
所以找到边界较短边更长,边界间距离更远的情况是关键,是否可以固定一个变量,去寻找另一个变量的更大值呢?完全固定不太能实现,但是可以让某个变量从最大值开始变小,在每个状态下寻找另一个变量的更大值求得面积最大,好吧,其实就是双指针实现。
双指针:数组两端同时遍历(或一端两个指针先后遍历),可以将时间复杂度降至 O ( n ) O(n) O(n) ,最关键的是确定指针移动条件。
本题如何设置双指针?
初始状态,设置双指针分别在首尾处,此时宽度是最大的,后面无论指针怎么移动宽度都是减小的,想要找到面积可能更大的情况关键在于找到移动后边界较短长度尽可能大的情况。可以证明较长边界的一边指针移动带来的变化一定不会使面积增大。
证明如下:
设起始宽度为 t 1 t_1 t1 ,左右边界高度分别为 h l , h r h_l, h_r hl,hr ,且有 h l < h r h_l<h_r hl<hr ,此时盛水面积为 a r e a 1 = t 1 ∗ h l area_1=t_1*h_l area1=t1∗hl
移动右边界向左边界靠近有宽度 t 2 t_2 t2 ,且 t 2 < t 1 t_2<t_1 t2<t1 ,移动后的右边界高度为 h n e w _ r h_{new\_r} hnew_r 。
当 h n e w _ r ≥ h l h_{new\_r}≥h_l hnew_r≥hl ,移动后的盛水面积 a r e a 2 = t 2 ∗ h l area_2=t_2*h_l area2=t2∗hl ,因为 t 2 < t 1 t_2<t_1 t2<t1 ,所以 a r e a 2 < a r e a 1 area_2<area_1 area2<area1 。
当 h n e w _ r < h l h_{new\_r}<h_l hnew_r<hl ,移动后的盛水面积 a r e a 2 = t 2 ∗ h n e w _ r area_2=t_2*h_{new\_r} area2=t2∗hnew_r ,两个因子都变小了,面积自然也变小了。
所以说,移动边界较长的那一边无论怎么移动都不会带领我们找到面积更大的方案,就不要随便移动啦。
再看看移动较短的那一边会是什么情况:
初始情况同上。
移动左边界向右边界靠近得到宽度 t 3 t_3 t3 ,且 t 3 < t 1 t_3<t_1 t3<t1 ,移动后的左边界高度为 h n e w _ l h_{new\_l} hnew_l 。
当 h n e w _ l ≥ h r h_{new\_l}≥h_r hnew_l≥hr ,移动后的盛水面积 a r e a 3 = t 3 ∗ h r area_3=t_3*h_r area3=t3∗hr ,此时 t 3 < t 1 , h r > h l t_3<t_1, h_r>h_l t3<t1,hr>hl 一大一小,对于乘法运算来说没法确定其变大还是变小,但是至少有机会变大。
当 h n e w _ l < h r h_{new\_l}<h_r hnew_l<hr ,移动后的盛水面积 a r e a 3 = t 3 ∗ h n e w _ l area_3=t_3*h_{new\_l} area3=t3∗hnew_l ,此时 h n e w _ l h_{{new\_l}} hnew_l 与 h l h_l hl 的大小关系不知,若 h n e w _ l ≤ h l h_{new\_l≤}h_l hnew_l≤hl ,那么两个因子都变小,面积也一定变小;若 h n e w _ l > h l h_{new\_l>h_l} hnew_l>hl ,一大一小,也存在着变大的可能。
虽然说移动较短的边带来的不一定是面积变大的结果,但至少是有希望的,还是得冲一冲,万一撞见了最大值呢?就跟人生一样,未知至少是有希望的,不拼一拼怎么知道自己有多少可能性呢!哈哈,所以要移动值较小的那一边的指针咯。
至于两边一样长的情况嘛,随便移哪一边都一样,可以默认移动左边。
那两边一起移动呢?可以嘛?一起移会错过很多情况,也不一定能遇到最大值的,不够稳妥,所以每移动一步都需要停下来判断一下,移动哪一边一定不会遇到最大值,那就不要移动那边,相当于舍弃了移动那一边所有风景了,我们要选择有希望的那条路。也就是每次移动边界较短的那一边指针。
代码实现:
class Solution {
public int maxArea(int[] height) {
int i=0, j=height.length-1; // 初始化指针
int water = 0; // 最大盛水量
while(i<j){
// 计算盛水面积
water = Math.max(water, (j-i)*Math.min(height[i], height[j]));
// 消除当前“短板”,寻找更大可能
if(height[i]<=height[j])i+=1;
else j-=1;
}
return water;
}
}
可能看到这里还是会有所疑问,凭什么说像这样的规则一定就能找到最大值呢?会不会因为短边的一次移动就错过了最大值的情况呢?当然不会,让我们来看一看每次剔除掉的情况是什么样子的:
算法图解(视频传上去不知道为啥有点糊,凑合看吧,哈哈)