1. 题目描述
2. 解题思路
愣头青的我遇到这种题目还是想妄想先用暴力解法试一下,虽然心中已经做好了要超时的打算,但还是忍不住先写一下唯一能下手的思路。果然不出意外传统的暴力解法是超时的,鉴于我一下子也想不出来其他解法,于是我又开始萌生了优化这个暴力算法使其变得可用的念头。
思考了一番两次for
循环是雷打不动优化不掉的了,那咋整呢?只能是让某些循环跳过或停止(避免一些无意义的循环计算)。大家应该都能想得到,第一层循环是决定水箱的左边,第二层循环是决定水箱的右边,那是什么情况下水箱的右边再高也没用呢(不能超越前最大值)?没错,就是在左边较矮的时候。较矮是多矮?临界值怎么界定呢?我们的目标是找出容积最大值,因此如果该较矮值与步数相乘都比当前最大值小,那么就没有必要去寻找右边了(右边比左边更高改变不了结果,更矮的话结果更差)。带着这个跳过某些循环的思路就可以容易地算出该高度的临界值是max/length_for_second_loop
。
有点难理解?来看看一个例子,数组是 [1,8,6,2,5,4,8,3,7],j
从i+1
开始。
- 初始状态
max=0
,height[0]=1
,以坐标0为左边,容器最大的步长为length-i-1=9-0-1=8
,因此以height[0]
为左边的最大容积理想情况可达到1*8=8>max
,因此该层循环要计算。 - 经过上一次循环后,
max=8
,height[1]=8
,以坐标1为左边,容器的最大步长为length-i-1=9-1-1=7
,因此以height[1]
为左边的最大容积理想情况可达到8*7=56>max
,因此该层循环要计算。 - 经过上一次循环后,
max=49
,height[2]=6
,以坐标2为左边,容器的最大步长为length-i-1=9-2-1=6
,因此以height[2]
为左边的最大容积理想情况可达到6*6=36<max
,该层循环跳过。 max=49
,height[3]=2
,length-i-1=9-3-1=5
,2*5=10<max
,跳过。- …跳过
- …跳过
- …跳过
- …跳过
- …跳过
图示为value[i][j]
的值,表示左边为height[i]
,右边为height[j]
的容积,有数值代表经过计算,显然该算法跳过了5个循环,能够在一定程度上提高暴力效率。
ok,上述优化思路是把某部分的循环跳过,那么更激进一点,我们可不可以让某些无法跳过的循环早点停止呢?怎么才能早点停止?是不是意味着该循环再循环下去的值也不会比我现在的值更大了?在不考虑容器的高度的情况下,步长(底面积)越大其容积就越大,虽然这里的容器高度是很大影响的,但我们还是这样期盼着对吗?因此在设计第二层循环时候,我们可以采用逆序遍历,先从离左边最远的右边开始计算容积,然后同样的把左边当成最小值,如果该最小值乘以剩下的步数也不够当前的最大值大,那就需要提取终止该循环了,所以该早停思路的临界条件就是j-i≥max/height[i]
。
有点绕?再来看一个例子,数组仍然是 [1,8,6,2,5,4,8,3,7],j
从length-1
开始。
- 初始状态
max=0
,height[0]=1
,j=8
,以坐标0为左边,可执行的最大步长为j-i=8
,因此以height[0]
为左边的最大容积理想情况可达到1*8=8>max
,因此该层的j
坐标要计算。 - 经过上一个值的计算后
max=8
,height[0]=1
,j=7
,以坐标0为左边,可执行的最大步长为j-i=7
,因此以height[0]
为左边的最大容积理想情况可达到1*7=7<max
,因此该层的j
坐标及其后的坐标都不需要计算了,停止该层循环。 - 经过上一层的计算后
max=8
,height[1]=8
,j=8
,以坐标1为左边,可执行的最大步长为j-i=7
,因此以height[1]
为左边的最大容积理想情况可达到8*7=56>max
,因此该层的j
坐标要计算。 - 经过上一层的计算后
max=49
,height[1]=8
,j=7
,以坐标1为左边,可执行的最大步长为j-i=6
,因此以height[1]
为左边的最大容积理想情况可达到8*6=48<max
,因此该层的j
坐标及其后的坐标都不需要计算了,停止该层循环。 max=49
,height[2]=6
,j=8
,j-i=6
,6*6=36<max
,跳过。- …跳过
- …跳过
- …跳过
- …跳过
同样的,图示为value[i][j]
的值,表示左边为height[i]
,右边为height[j]
的容积,有数值代表经过计算,显然该算法只计算了两个值,又进一步提高了暴力的执行效率。
没接触过此类型题目的常人能想到的思路描述完了,虽然性能远远比不上官解思路,但我感觉还是挺有意思的,但官解思路是那种“你没有碰见过你就不会做”,只能慢慢积累了。
对于官解提出的双指针解法确实很妙,理解起来很容易,但是难点其实在于其正确性证明的部分,这里不过多描述了,有兴趣可以去看看官解,这里贴上一个我认为挺有意思的哲学性描述吧!
3. 代码实现
3.1 顺序暴力+跳过
public int maxArea(int[] height) {
int length = height.length;
int max = 0;
for (int i = 0; i < length - 1; i++) {
if (height[i] * (length - i - 1) < max)
continue;
for (int j = i + 1; j < length; j++) {
max = Math.max((j - i) * Math.min(height[i], height[j]), max);
}
}
return max;
}
3.2 逆序暴力+早停
public int maxArea(int[] height) {
int length = height.length;
int max = 0;
for (int i = 0; i < length - 1; i++) {
if (height[i] == 0)
continue;
for (int j = length - 1; j > i && (j - i) * height[i] >= max; j--) {
max = Math.max((j - i) * Math.min(height[i], height[j]), max);
}
}
return max;
}
3.3 双指针
public int maxArea(int[] height) {
int left = 0, right = height.length - 1, max = 0;
while (left < right) {
if (height[left] < height[right]) {
max = Math.max(max, (right - left) * height[left]);
left++;
} else {
max = Math.max(max, (right - left) * height[right]);
right--;
}
}
return max;
}
3.4 对比
暴力解法的时间复杂度为O(n²),双指针解法的时间复杂度是O(n),但其实优化后暴力解法的实际复杂度已经远比O(n²)小了,不然也会时间超限;三种解法的空间复杂度都是O(1)。