一、题目:盛最多的水
给你 n
个非负整数 a1,a2,...,an
,每个数代表坐标中的一个点 (i, ai)
。在坐标内画 n
条垂直线,垂直线 i
的两个端点分别为 (i, ai)
和 (i, 0)
。找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1]
输出:1
示例 3:
输入:height = [4,3,2,1,4]
输出:16
示例 4:
输入:height = [1,2,1]
输出:2
提示:
n = height.length
2 <= n <= 3 * 104
0 <= height[i] <= 3 * 104
二、解决方案
本题使用双指针法进行解答。设计两个指针 left
和 right
分别指针容器的两端,即指针 left = 0
,right = height.length - 1
。此时,容器的容量可通过如下公式进行计算:
maxContainer = (两个指针之间的距离) * (两个指针指向的元素中的最小值)
算法流程如下图所示:
三、双指针法移动规则(即每次移动高度较小的一边)有效性验证
- 设每一个状态下的容器容量为
S(left, right) (0 <= left < right <= height.length - 1)
,则S(left, right) = (right - left) * Math.min(height[left], height[right])
。 - 无论是移动较短的一边还是较长的一边,都会使得
(right - left)
减小 1。因此,我们。只关心在移动过程中height[left]
或height[right]
是变小,还是变大,还是保持不变。 - 对于较短的一边,此时是它决定当前状态的容器容量。则它移动之后有三种可能:变小、不变,变大,对应的
S(left, right)
的变化有:变小、变小、变大。 - 对于较长的一边,它移动之后也有三种可能:变小、不变、变大,对应的
S(left, right)
的变化有:变小、变小、变小。因为此时决定容器容量的不是它的高度,且(right - left)
变小。
综上所述,每次移动高度较小的一边,能够有机会得到更大的容器容量。
四、背后的原理-缩减搜索空间思想
此题中,假设一共有 n
个柱子,编号为 0,1,2,...,n
,其高度分别为 H0,H1,H2,...,Hn
。移动过程中的柱子 i
和 j
满足约束条件:i
和 j
都是合法的柱子下标,即 0 <= i < j <= n
。移动柱子的目标是为了得到一组 (i, j)
使得容器的容量最大。以 n = 8
为例,此时全部的搜索空间如下所示:
由于受 i
和 j
约束条件的限制,搜索空间是白色的倒三角部分。可以看到,搜索空间的大小是 O(n^2)
数量级。若用暴力解法求解,一次只检查一个单元格,那么时间复杂度一定是 O(n^2)
。要想得到 O(n)
的解法,我们就需要能够一次排除多个单元格。接下来说明缩减搜索空间的思想:
一开始,我们检查右上方单元格 (0,7)
,即考虑最左边的 0
号柱子和最右边的 7
号柱子,计算它们之间容器容量。然后我们比较一下两根柱子的高度,关注其中较短的一根。
假设左边的 0
号柱子较短。则 0
号柱子的高度即为容器当前高度的上限。由于 7
号柱子是离 0
号柱子最远的,水的宽度也就最大。若换其他的柱子和 0
号柱子配对,水的宽度只会更小,高度也不会增加,容纳水的面积只会更小。所以 0
号柱子和 6,5,…,1
号柱子的配对都可以排除掉了,这就相当于 i=0
的情况全部被排除,只记录 (0,7)
这组结果。
对应于双指针解法的代码,就是 left++
;对应于搜索空间,就是削减了一行的搜索空间,如下图所示。
排除掉了搜索空间中的一行之后,我们再看剩余的搜索空间,仍然是倒三角形状。我们检查右上方的单元格 (1,7)
,即考虑 1
号柱子和 7
号柱子,计算它们之间容纳水的面积。然后,比较两根柱子的高度。
假设此时 7
号柱子较短。同理, 7
号柱子的高度即为容器当前高度的上限,若换其他的柱子和 7
号柱子配对,水的宽度变小,高度也不会增加,容纳水的面积只会更小,即 7
号柱子和 2,3,…,6
号柱子的配对都可以排除掉了。这相当于 j=7
的情况全部被排除,只记录了 (1,7)
这组结果。
对应于双指针解法的代码,就是 right--
;对应于搜索空间,就是削减了一列的搜索空间,如下图所示。
综上所述,无论柱子 i
和 j
哪根更短,我们都可以排除掉一行或者一列的搜索空间。经过 n
步以后,就能排除所有的搜索空间,检查完所有的可能性。搜索空间的减小过程如下所示。
五、程序实现过程
Java 解法一:
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int maxContainer = Integer.MIN_VALUE;
while (left < right) {
int width = right - left;
int h = Math.min(height[left], height[right]);
// 计算容器的最大容量
maxContainer = Math.max(maxContainer, width * h);
// 移动高度较小的边
if (height[left] < height[right]){
left++;
continue;
}
right--;
}
return maxContainer;
}
Java 解法二:使用三目运算符 A ? B : C
,此处有一点需要注意,必须先做 (right - left)
的计算,然后再进行高度的计算和移动,即 Math.min(height[left++], height[right])
或 Math.min(height[left], height[right--])
,这是移动(自增)操作会影响 (right - left)
的结果。
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int maxContainer = Integer.MIN_VALUE;
while (left < right){
maxContainer = height[left] < height[right] ?
Math.max(maxContainer,(right - left) * Math.min(height[left++],height[right])) :
Math.max(maxContainer,(right - left) * Math.min(height[left],height[right--]));
}
return maxContainer;
}
Java 解法三:相对于上面两种解法,对于移动前后高度相同的,不再进行容器容量的计算。
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int maxContainer = Integer.MIN_VALUE;
while (left < right) {
int width = right - left;
int h = Math.min(height[left], height[right]);
// 计算容器的最大容量
maxContainer = Math.max(maxContainer, width * h);
// 若下一轮 while 循环仍和 h 相等,则说明移动前后高度没有发生变化,此时继续移动,且不计算容器容量
// 若 h == height[left],说明 left 是高度更小的边
while (left < right && h == height[left]){
left++;
}
// 若 h == height[right],说明 right 是高度更小的边
while (left < right && h == height[right]){
right--;
}
}
return maxContainer;
}