题目
给定一个长度为n的整型数组height,有n条垂线,第i条线的两个端点是(i,0),(i,height(i)),找出其中的两条线,使得他们与x轴共同构成的容器可以容纳最多的水。返回容器可以存储的最大水量。

示例:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
分析
- 最大水量由宽高乘积决定,假设我们找到了其中的两条线,他们的宽高乘积最大,那么宽是两个元素下标差值max(i,j)-min(i,j),高是max(m,n) - min(m.n),所以整个过程有两步,第一步计算乘积,第二步,比较乘积大小。
- 那么第一步计算乘积如何进行了,这一步是最关键的一步,当然比较大小也可以放到计算乘积过程中,我们只需要在每一次计算水量乘积的时候取最大值即可。
计算乘积,需要选择两个点,简单的思路是遍历枚举,但是此方法复杂度过高,那有没有什么方法能过滤掉其中的一部分没用的枚举呢?
这时我们思考,乘积最大,那就是高最大,宽最大,于是我们需要找到最大的高对应点,从此点对应的下标开始找到最大的宽。但是我们寻找最大高,时间复杂度已经是O(N),如果还有后续的遍历时间复杂度会大于O(N),从这点上考虑已经不适合了,于是考虑换思路。假设我们还是从第一个位置开始遍历,由于需要确定水量,必须要直到另外一个点的位置,那如何选择第二个点的位置呢?如果能想到这一点,就很接近最优解了。
按照我们的假设,选中第一个点如果从第二个点开始选依次遍历那么又回到了我们开始的思想-枚举,也就是一一枚举,复杂度毋庸置疑是很高的,那么又如何选择第二个点呢?
那我们考虑一下从第二个点开始选择一直往后,所有点的选择几乎是一样的,除了最后一个点,为什么说是一样的,那是因为我们既要枚举前面的点,又要枚举后面的点,势必增加了复杂度,可是如果我们选择最后一个点,那么问题就简单多了,我们只需要考虑从最后一个点往前枚举;至此我们确定了点的选择顺序。这是一个怎样的顺序呢?我们换一种描述方式:
由于需要确定水量,我们需要界定左边的点的位置和右边的点的位置,我们从数组的边界开始,依次遍历,那么在这个过程中,会有一个什么样的现象?需要不断的观察,左边的点可以移动,右边的点也可以移动,那如何移动,有什么规律吗?为了便于分析我们来模拟一下这个过程:
第一次,左右两边边界对应的水量为min(1,7) * (x2-x1) = 1 * 8 = 8
假设我们移动左边点,此时对应的水量为:min(8,7) * (x2- x1) = 7 * 7 = 49
假设我们移动右边点,此时对应的水量为:min(1,3) * 7 = 7,很显然,一个面积变小,一个面积变大,有规律吗?
那我们看一下其中的变量,首先宽肯定是变小了,那么水量一定会变小吗,很显然不一定,因为高可能变大,但是我们的目的是什么?是找出最大水量,那么变小的可能性我们是不是需要排除掉?是的,那为什么移动右边的点水量会变小呢,因为同等宽下,我们移动右边点得到的高变小了,也就是min(1,3,7)取最小值还是1,那可能就是这个较小的左边值影响了整个的水量,于是我们需要考虑移除掉,
此时规律变成了:每次我们需要移动点位时,需要移除较小值高的点,大致时这样一个过程,因为如果移动较大值高的点,整个水量会变得比原来还小,这不是我们期望的。
有了这样的思路,我们可以开始实现:
func maxArea(height []int) int {
var area int
// 定义左右点位
left, right := 0, len(height)-1
// 开始遍历循环,直到两个点重合,循环结束,
for left != right {
var a1 int
if height[left] <= height[right] {
a1 = (right - left) * height[left]
left++
} else {
a1 = (right - left) * height[right]
right--
}
// 将当前水量和上一次水量比较
area = max(a1, area)
}
return area
}
提交测试:
62/62 cases passed (58 ms)
Your runtime beats 46.88 % of golang submissions
Your memory usage beats 88.54 % of golang submissions (7.5 MB)
通过!
官方解题思路比较清晰,是一个双指针问题,主要流程也是寻找其中的规律,我们截取其中一段:
双指针代表的是 可以作为容器边界的所有位置的范围。在一开始,双指针指向数组的左右边界,表示 数组中所有的位置都可以作为容器的边界,因为我们还没有进行过任何尝试。在这之后,我们每次将 对应的数字较小的那个指针 往 另一个指针 的方向移动一个位置,就表示我们认为 这个指针不可能再作为容器的边界了。
为什么对应的数字较小的那个指针不可能再作为容器的边界了?
在上面的分析部分,我们对这个问题有了一点初步的想法。这里我们定量地进行证明。
考虑第一步,假设当前左指针和右指针指向的数分别为 x 和 y,不失一般性,我们假设 x≤y。同时,两个指针之间的距离为 t。那么,它们组成的容器的容量为:min(x,y)∗t=x∗t
我们可以断定,如果我们保持左指针的位置不变,那么无论右指针在哪里,这个容器的容量都不会超过 x∗t 了。注意这里右指针只能向左移动,因为 我们考虑的是第一步,也就是 指针还指向数组的左右边界的时候。
我们任意向左移动右指针,指向的数为 y1,两个指针之间的距离为 t1,那么显然有
t1<t,并且 min(x,y1)≤min(x,y):
如果
y1≤y,那么 min(x,y)≤min(x,y);
如果 y1>y,那么 min(x,y1)=x=min(x,y)。
因此有:
min(x,y1)∗t1<min(x,y)∗t
即无论我们怎么移动右指针,得到的容器的容量都小于移动前容器的容量。也就是说,这个左指针对应的数不会作为容器的边界了,那么我们就可以丢弃这个位置,将左指针向右移动一个位置,此时新的左指针于原先的右指针之间的左右位置,才可能会作为容器的边界。