当前位置 博文首页 > Container With Most Water_让代码改变世界:LeetCode 11

    Container With Most Water_让代码改变世界:LeetCode 11

    作者:[db:作者] 时间:2021-07-10 18:58

    今天来写leetcode第11题,这个题目是一道“应用题”,不过并不存在理解题意方面的问题。题目确实是考察算法的,这么说是因为解题的思路有很多种,而其中有一个最简单的,不过这可不是轻轻松松就能找到的。

    原题如下:

    Given?n?non-negative integers?a1,?a2, ...,?an, where each represents a point at coordinate (i,?ai).?n?vertical lines are drawn such that the two endpoints of line?i?is at (i,?ai) and (i, 0). Find two lines, which together with x-axis forms a container, such that the container contains the most water.

    Note: You may not slant the container.

    题目的意思大概是这样的,给定一个非负整数数组,然后把某个数所在位置(数组下标)作为横坐标而数字本身作为纵坐标,这样在x轴上方会画出很多个点,再通过这些点做x轴的垂线,最后就得到了很多垂直的线,把这些线看做容器壁,x轴看做容器底,问哪条线和x轴构成的容器称的水最多,求出这个最大的容量。

    这个题的解法可以说是“飘在水面”上的,显然可以通过蛮力法来解决这个问题,即每两个线都组合试一下,看哪个最大。这样一来会形成时间复杂度为n(n-1)/2(即O(n^2))的算法,这样的算法显然是“太幼稚了”。我们应该试图改进这种算法,或者开辟新的算法,可惜我只是改进了这个蛮力法并没有找到所谓的最优算法,但改进的效果还是可以的,所以还是给大家说一下,稍后再介绍最优算法。

    我的思路是这样的,因为数组并没有什么特殊要求,所以假设其为随机数,这样一来那显然会有一部分线是可以提前排除的,也就是说是不可能用到它的。比如,如果一根线左边和右边都有比它长的线,那显然这条线不可能作为最大容器的一部分,因为如果它是最大容器的一部分那很容易证明容器还可以扩容:如果它在水的左边,那可以再扩展到左边那条比它长的线,同理如果它在水的右边可以扩容到右边比它长的线。总之,中间是短线的不可能起作用,这样可以“删除”一部分线。再对剩下的两个组合。

    通过代码设计,发现这种算法对应随机输入的数组确实有比较好的效果,因为很多线都被删去了。但对于一些特殊输入,如连续增长的数组,算法效果非常不好,因为算法不能删去任何线反而浪费了时间。以至于当输入较大时算法超时了。

    再一次改进还是基于上面的思路,不过这次把左边和右边开了,因为前面的算法实际剩下的是可以做左线或者可以做右线的线,我们还可以将其分开,即那些是只能做左线的,那些是只能做右线的。这么说可能有点儿问题,其实我们找到的是那些“也许会在最后结果里做左线的线”和“也许会在最后结果里做右线的线”。代码如下

    class Solution {
    public:
        int maxArea(vector<int>& height) {
    		vector<int> lefInAction;
    		int len=height.size();
    		int max=0;
    		for(int i=0;i<len;++i)
    		{
    			if(height[i]>max)
    			{
    				max=height[i];
    				lefInAction.push_back(i);
    			}
    		}
    		max=0;
    		vector<int> rigInAction;
    		for(int i=len-1;i>=0;--i)
    		{
    			if(height[i]>max)
    			{
    				max=height[i];
    				rigInAction.push_back(i);
    			}
    		}
    
    		int lenLefAct=lefInAction.size();
    		int lenRigAct=rigInAction.size();
    
    		int maxCap=0;
    		int cap=0;
    		for(int i=0;i<lenLefAct;++i)
    		{
    			for(int j=0;j<lenRigAct;++j)
    			{
    				cap=(rigInAction[j]-lefInAction[i])*min(height[lefInAction[i]],height[rigInAction[j]]);
    				if(cap>maxCap)maxCap=cap;
    			}
    		}
    		
    };
    这样一来代码对特殊输入的改进也是比较大的了,所以程序通过了验证,时间为28ms。这个时间所含信息量比较多,我们一会儿再说。

    下面说大神的思路,也就是本题的最优算法。我们这次先上代码(在这里感谢StefanPochmann大神的分享)

    class Solution {
    public:
    
        int maxArea(vector<int>& height) {
        int water = 0;
        int i = 0, j = height.size() - 1;
        while (i < j) {
            int h = min(height[i], height[j]);
            water = max(water, (j - i) * h);
            while (height[i] <= h && i < j) i++;
            while (height[j] <= h && i < j) j--;
        }
        return water;   
        }
    };
    我想这个代码以及所利用的算法并不是很难理解,反正我看了以后有一种恍然大悟的感觉,或许这就是算法的魅力吧。

    为了完整性以及不同读者的水平,我们还是来对其做个说明吧:显然其算法的时间复杂度为O(n),所以这应该是最优算法了吧,不过这个问题我不会证明,抱歉,反正直观上理解这个算法是非常“好”的。两个指针分别指向数组头和数组尾,然后比较哪个线比较短,把比较短的往中间移动直到遇到一个比其长的线,在这个过程中跟新容量的最大值。就是这么简单,要想证明这个算正确,我们只需证明“移动所错过的组合中不可能有最大容量”,比如就第一步移动吧,加入左边的比较短,那么左边会移到第二个位置,那么这样会错过所有第一个和除最后一个外其他的线的组合。也就是说你并没有判断第一个和第二是多少,和第三个是多少。。。那么这些错过的里面会有最大容量吗?显然不会,因为左边这个是比较短的,也就是说容器的深度是由它限制的,它和除最后一个外其他线组合时,水的深度不可能超过它,而容器底却一定会缩小,所以这个容器的容量一定会缩小的。其他的情况和此类似。

    大神的代码运行时间为24ms,这一点可能有些意外,不过更意外的是统计数据显示这已经是最快的了(没有少于24ms的了),但同时又显示仅仅击败了百分之60多的C++代码,这个不知道什么原因,或许是统计问题吧。至于为什么只比我的“改进算法”代码只快了4ms我想是因为测试实例较少,算法的优越性没有完全体现出来吧,因为无论从哪种角度来说这个代码都是优于我的代码的。

    这一道题难度不大,但不失为一道考察算法的好题,通过蛮力法、改进的蛮力法、最优算法的学习过程,我们对算法本身会有更多的了解,对算法的魅力也会有更多的领略。

    最后呢,说几句题外话吧,最近一篇名为《阿里缩招(从3000人大幅砍到400人)降薪,互联网寒冬要来了吗?》的文章挺火的,大家可以百度看一下。从标题可以看出,这篇文章是对互联网行业的未来不看好的,因为搞程序搞算法的嘛,所以大家对这个可能比较感兴趣,本人也是这两年要毕业,所以对这些东西比较关注。至于文章写得有没有道理,是否会对你产生影响,那就看各位对这个事情的看法了。不过最后还是那句话,希望每一个坚持的人都能实现自己的理想。加油吧,筒子们


    cs
    下一篇:没有了