原题
给定n个非负整数a1,a2,…,an,每个数代表点(i,ai)。坐标系上有n条垂直于x轴的线段,每条线段的两个端点分别是(i,ai)和(i,0)。找到其中的两条线段,使它们和x轴组成的容器的容积最大。
注意:容器不能倾斜,n至少为2.
来源:LeetCode
链接:https://leetcode.com/problems/container-with-most-water/
解析
考虑这道题的时候,最直接的思路就是暴力,把所有可能的容积都求出来,保留最大值。但这样做的时间复杂度是O(n2),显然不是最理想。
解法一:暴力
代码:
class Solution {
public:
int maxArea(vector<int>& height) {
int maxA = 0;
for(int i = 0; i < height.size(); ++i){
int l = height[i];
for(int j = i+1; j < height.size(); ++j){
int r = height[j];
int tmp = (j-i)*(l<=r? l : r);
if(tmp > maxA)
maxA = tmp;
}
}
return maxA;
}
};
运行情况如下:
Runtime: 728 ms
Memory Usage: 9.8 MB
然后考虑过动态规划,但是会发现根本没办法界定合适的边界,各个子问题之间也没有依赖关系。因为计算“容积”需要考虑的不仅是两条边的高度大小,还有底边的长度,这是动态规划没有办法通过简单的边界条件表示出来的。
解法二:双指针
双指针在这种与线性序列相关的问题中常被使用,尤其是链表。在这道题中,首尾各有一个指针代表目前考察的容器边界,时时更新目前最大容积值。那么,指针应该怎么移动呢?
其中有一个规律,由于每个容器的“容积”只取决于较短的边和底边的长度,那么,对于当前的边界ai和aj,不妨设height[ai]<height[aj],当前的容积x*height[ai](假设x是底边长度),此时不管移动左指针还是右指针(都向内移动,解释见下),x都必然减小,所以现在要做的就是使得移动后,较短的边可能会比height[ai]长一点。所以,如果移动右指针,较短的边依旧<=height[ai],而如果移动左指针,较短的边有可能会比height[ai]长。
因此,每次计算好当前容器的容积之后,就将较短边一方的指针向内移动。
(为什么指针不会再向外移动呢(即左指针向左,右指针向右)?以左指针为例,因为它此前必然是由于指向的边较短才向右移动(而在移动之前已经得到了当时容器的容积x*h,设为V),所以无论现在右指针移动了多少,如果再把左指针向左移回去,首先,底边长度必然<=x,而较短边的高度也是<=h,那么得到的容积一定<=V,可见对于找更优解是没有帮助的。)
显然,这样做的时间复杂度为O(n)。顺便提一句,由于解题必须至少要将height全部读一遍,因此问题的计算复杂度的下界就为O(n),也即双指针法是解这道题的最好的算法。
代码如下:
class Solution {
public:
int maxArea(vector<int>& height) {
int lp = 0; //左指针
int rp = height.size()-1; //右指针
int maxA = 0;
while(lp < rp){
int lh = height[lp];
int rh = height[rp];
bool flag = lh <= rh;
int tmp = (rp-lp)*(flag? lh : rh);
if(tmp > maxA)
maxA = tmp;
if(flag)
lp++;
else
rp--;
}
return maxA;
}
};
运行情况:
Runtime: 16 ms
Memory Usage: 9.7 MB
这道题的关键在于要能想到使用双指针,且能发现其中的规律,一旦确定了算法,代码写起来其实很简单。
如有错误及不足,欢迎交流指正~