zoukankan      html  css  js  c++  java
  • [LeetCode] Gas Station,转化为求最大序列的解法,和更简单简单的Jump解法。

    LeetCode上 Gas Station是比较经典的一题,它的魅力在于算法足够优秀的情况下,代码可以简化到非常简洁的程度。

    原题如下

    Gas Station

    There are N gas stations along a circular route, where the amount of gas at station i is gas[i].

    You have a car with an unlimited gas tank and it costs cost[i] of gas to travel from station i to its next station (i+1). You begin the journey with an empty tank at one of the gas stations.

    Return the starting gas station's index if you can travel around the circuit once, otherwise return -1.

    Note:
    The solution is guaranteed to be unique.

    class Solution {
    public:
        int canCompleteCircuit(vector<int> &gas, vector<int> &cost) {
        }
    };

    最直观的思路

    最直观的解法自然是O(n2) 时间复杂度的挨个尝试。这里不做讨论。

    思路变化一

    如果你是司机,你肯定会选这么样的一个站点:从这个站点出发,先遇到的站点都是给车加油大于耗油的,从而让车攒够足够的余油,来撑过剩下的那些耗油大于加油的站点。

    对于站点 i,我们把 gas[i] - cost[i] 当作整体考虑,我们用diff[i] = gas[i] - cost[i] 表示从当前站点出发,到下一个站点后剩余的油量。

    因此结合司机的经验,题目就是需要找出这么一个点,从这个点出发,diff的累加值能达到峰值。

    想起了什么吗?这道题就是求diff[]数组上的 和最大连续子序列,这个最大子序列的起始点就是车的出发点!如果车从这儿开都不能做到油箱不空,那么这个题目就该返回-1了。

    因此,这道题转化成了 求循环数组的最大子序列问题

    这种问题的求法,主要就是利用动态规划,求出diff[0] 到 diff[size-1]这个非循环数组上的最大子序列MAX和最小子序列MIN。然后比较MAX和 Total - MIN,选出较大值(Total为 diff数组所有元素和)。当然,我们要求的不是最大子序列的和,而是最大子序列的其实位置。因此,如果MAX 较大,返回MAX所代表的最大子序列的起始元素;否则,返回MIN所代表的最小子序列末尾的下一个元素。

    参照这种思路,我们可以写出如下代码:

    class Solution {
    public:
        int canCompleteCircuit(vector<int> &gas, vector<int> &cost) {
            if(gas.size() != cost.size()) return -1;
            if(gas.size() == 0) return -1;
            int MAX = gas[0] - cost[0], MIN = gas[0] - cost[0], max = gas[0] - cost[0], min = gas[0] - cost[0];
            int stmax = 0, stMAX = 0, endMIN = 0;
            int total = 0, diff = 0;
            for(int i = 0; i < gas.size(); ++i){
                diff = gas[i] - cost[i];
                total += diff;
                
                if(max < 0){
                    max = diff;
                    stmax = i; //stmax用来存储当前序列的起始点
                }else max += diff;
                if(max > MAX){
                    MAX = max;
                    stMAX = stmax; //stmax用来存储最大序列的起始点
                }
                
                if(min > 0) min = diff;
                else min += diff;
                if(min < MIN){
                    MIN = min;
                    endMIN = i; //endMIN用来存储最小序列的末尾点
                }
            }
            return total < 0 ? -1 : (MAX >= (total - MIN) ? stMAX : (endMIN+1) % gas.size()); //通过比较 MAX 和 (total - MIN) 返回相应结果。
        }
    };

    这样的解法的时间复杂度是 O(N)

    在末尾可以发现,最后我们在通过比较 MAX 和 (total - MIN) 得出结果后,并没有模拟让车从所得站点跑一圈,来验证是否正确,而是直接通过比较 total 和 0的关系来确定是否有解。

    如果Total >= 0,那么直接返回站点,如果Total < 0,返回-1。

    也就是说,直接通过Total的值就可以来确定该题是否有解

    思路变化二

    这道题最直观的思路,是逐个尝试每一个站点,从站 i 点出发,看看是否能走完全程。如果不行,就接着试着从站点 i+1出发。

    假设从站点 i 出发,到达站点 k 之前,依然能保证油箱里油没见底儿,从k 出发后,见底儿了。那么就说明 diff[i] + diff[i+1] + ... + diff[k] < 0,而除掉diff[k]以外,从diff[i]开始的累加都是 >= 0的。也就是说diff[i] 也是 >= 0的,这个时候我们还有必要从站点 i + 1 尝试吗?仔细一想就知道:车要是从站点 i+1出发,到达站点k后,甚至还没到站点k,油箱就见底儿了,因为少加了站点 i 的油。。。

    因此,当我们发现到达k 站点邮箱见底儿后,i 到 k 这些站点都不用作为出发点来试验了,肯定不满足条件,只需要从k+1站点尝试即可!因此解法时间复杂度从O(n2)降到了 O(2n)。之所以是O(2n),是因为将k+1站作为始发站,车得绕圈开回k,来验证k+1是否满足。

    等等,真的需要这样吗?

    我们模拟一下过程:

    a. 最开始,站点0是始发站,假设车开出站点p后,油箱空了,假设sum1 = diff[0] +diff[1] + ... + diff[p],可知sum1 < 0;

    b. 根据上面的论述,我们将p+1作为始发站,开出q站后,油箱又空了,设sum2 = diff[p+1] +diff[p+2] + ... + diff[q],可知sum2 < 0。

    c. 将q+1作为始发站,假设一直开到了未循环的最末站,油箱没见底儿,设sum3 = diff[q+1] +diff[q+2] + ... + diff[size-1],可知sum3 >= 0。

    要想知道车能否开回 q 站,其实就是在sum3 的基础上,依次加上 diff[0] 到 diff[q],看看sum3在这个过程中是否会小于0。但是我们之前已经知道 diff[0] 到 diff[p-1] 这段路,油箱能一直保持非负,因此我们只要算算sum3 + sum1是否 <0,就知道能不能开到 p+1站了。如果能从p+1站开出,只要算算sum3 + sum1 + sum2 是否 < 0,就知都能不能开回q站了。

    因为 sum1, sum2 都 < 0,因此如果 sum3 + sum1 + sum2 >=0 那么 sum3 + sum1 必然 >= 0,也就是说,只要sum3 + sum1 + sum2 >=0,车必然能开回q站。而sum3 + sum1 + sum2 其实就是 diff数组的总和 Total,遍历完所有元素已经算出来了。因此 Total 能否 >= 0,就是是否存在这样的站点的 充分必要条件

    这样时间复杂度进一步从O(2n)降到了 O(n)。

    基于这个思路,可以写出更加简洁的代码:

    class Solution {
    public:
        int canCompleteCircuit(vector<int> &gas, vector<int> &cost) {
            if(gas.size() == 0 || cost.size() == 0 || gas.size() != cost.size()) return -1;
            int total = 0, sum = 0, start = 0;
            for(int i = 0; i < gas.size(); ++i){
                total += (gas[i] - cost[i]);
                if(sum < 0){ //发现油箱空了,从下一个站点尝试
                    sum = (gas[i] - cost[i]);
                    start = i;
                }else
                    sum += (gas[i] - cost[i]);
            }
            return total < 0 ? -1 : start; //用total判断start 是否是满足要求的解
        }
    };

    这种解法其实依托于一个数学命题:

    对于一个循环数组,如果这个数组整体和 SUM >= 0,那么必然可以在数组中找到这么一个元素:从这个数组元素出发,绕数组一圈,能保证累加和一直是出于非负状态。

    我在这里给出比较简单的证明,来证明这个命题为真:用反证法,假设数组A[]的和SUM >= 0,对于所有元素都不能做到累加和能保持非负。我们取任意元素,下标为p,假设累加至下标为q-1时,累加和还能保持非负,但是加上A[q]后,累加和成了负数。那么就是 A[p] + .. + A[q] < 0 (1);  A[p] + .. + A[k] >= 0 (p <= k <= q-1) (2)。

    又因为SUM >= 0,因此A[q+1] + ... A[p-1] = SUM - (A[p] + .. + A[q]) > 0,也就是说,从q+1开始,能保证 A[q+1] + ... A[p-1] > 0 (3)。既然A[q+1] + ... A[p-1] > 0,那么联合结论(2),A[q+1] + ... A[p-1] + A[p] + .. + A[k] 也是正数,也就是说,从A[q+1]出发,能保证到达A[p], A[p+1].... , A[q-1] 时累加和都是非负数;到达A[q]时呢?正好绕一圈,累加和就是SUM,因此此时累加和也是非负数。

    那么,至此,我们可以保证从A[q+1] 出发,到达A[p], A[p+1].... , A[q] 时累加和都是非负数。剩余部分的部分是A[q+2], A[q+3]... A[p-1],只要继续证明到达这些元素时,累加和是非负即可。由于结论(3),我们已经知道 A[q+1] + ... A[p-1] > 0,因此这道题就是证明总和 >0 时,A[q+1] 到达 A[q+2] 至  A[p-1]中的任意元素,其和都依然大于等于0。

    之前用模拟的方式,大概给出了直观的推衍,但是暂时没能给出严格的数学证明(无视那段被我划掉的部分吧。。)。

  • 相关阅读:
    Exchange 2013与 Office Web Apps 整合
    SharePoint2013 以其他用户登录和修改AD域用户密码 功能
    sharepoint 2010 自定义页面布局
    sharepoint 2010 记录管理 对象模型
    SharePoint2010 对象模型 关联列表
    在SharePoint Server 2010中更改“我的网站”
    xenapp 6.5 客户端插件第一次安装总是跳到官网
    如何解决在Windows Server 2008 R2 上安装证书服务重启后出现 CertificationAuthority 91错误事件
    在Win7 Hyper-v虚拟机中挂接真实机的声卡
    win8 中如何删除 共享文件夹 用户名和密码
  • 原文地址:https://www.cnblogs.com/felixfang/p/3814463.html
Copyright © 2011-2022 走看看