环形加油站问题

问题


城市的环形路有n个加油站,第i个加油站的油量用gas[i]来表示,你有如下的一辆车:

它的油缸是无限量的,初始是空的

它从第i个加油站到第i+1个加油站消耗油量为cost[i]

现在你可以从任意加油站开始,路过加油站可以不断的加油,问是否能够走完环形路。如果可以返回开始加油站的编号,如果不可以返回-1。

此题目来自于leetcode:https://oj.leetcode.com/problems/gas-station/

注意,解决方案保证是唯一的。

证明 
一个环形轨道上有n个加油站,所有加油站的油量总和正好够车跑一圈。证明,总能找到其中一个加油站,使得初始时油箱为空的汽车从这里出发,能够顺利环行一圈回到起点。

数学归纳法 
1)当n=1时,结论显然成立(汽车从唯一的加油站加油并出发); 
2)假设当n=k时结论成立,即公路上有k个加油站,从其中第m个加油站出发,汽车可以绕长为S1的公路一周;(为方便讨论,我们可以将原来的环状公路从出发站“剪开”,“拉直”为直线公路)

那么当n=k+1时,即增加一个站(第k+1个站),它携带着新的汽油,公路也因此延长了S=S2-S1的长度,新的汽油刚好可供汽车行驶距离S(这样便满足题设条件).我们不妨将第k+1个站放在原来第m个站的前面且与它的距离是S,则汽车从第k+1个站加油并出发,向第m个站开去,则刚好能到达第m个站,由(1)可知汽车可以走完长度为S1的公路,还原问题即得:汽车可以绕长为S2的公路一周.即当n=k+1时结论也成立.

由(1)和(2),可知结论对任意正整数n都成立,命题得证!

分析


这个题目其实比较简单,只要充分的理解,我相信大家都能够解决的。

你的这辆车的油缸是无限量的,所以每个加油站的油都可以加到车里,但是关键是你得保证,你在第i站的时候,油缸中有的油量,可以支撑你到第i+1站,对于每一站都要如此,所以,并不是总的油量大于消耗量就可以了。要保证每一站都有足够的油可以走到下一站,到每一站,你的车的油量都大于等于0就可以了。

经过上面的分析,很显然,暴力一点,我们每个站都试一下呗,然后找到每一站的油量都大于等于0的那个走法,返回开始的加油站;没有就返回-1。这个解法是O(n^2)的时间复杂度。

我们通过观察上面的暴力方法的步骤,可以发现有很大的改进空间。

当我们从第0个加油站开始,判断是否可以走完,然后从第1个加油站开始,进行判断的时候,其实中间的计算已经做过了。反过来,我们如果计算好了从第1个加油开始,到某一个站时,油量为tank,此时考虑从第0个开始时,到该加油站的油量就是gas[i]-cost[i] + tank。

这时隐约觉得,解决方案的时间复杂度可以是O(n)的时间复杂度。

事实上确实可以,具体的方法如下: 
tank表示当前车的油缸里的油量

从第0个站开始,tank += gas[0] - cost[0],只要tank>=0,我们就继续到下一个加油站

当不满足tank>=0,顺着环路试验从前一个站开始,比如,n-1: tank += gas[n-1] - cost[n-1],如果还小于0,继续试验再前一个。

直到满足 tank>=0,再进行第1步,依次类推

当一前一后这两个相遇了,算法也就结束了,tank>=0,就成功,返回相遇的位置;否则,失败,返回-1

上面这个方法的时间复杂度是多少呢?O(n)的,很简单,我们作为一个整体来看,每一个节点都只走了一次。

代码



class Solution
{
public:
int canCompleteCircuit(const vector<int> &gas, const vector<int> &cost)
{
    vector<int> line(gas.size( ) << 1);

    int n = line.size( );

    for(int i = 0; i < n; i++)
    {
        line[i] = gas[i % gas.size( )]- cost[i % gas.size( )];
    }

    int start = 0;
    int oil = 0;
    for(int i = 0; i < n; i++)
    {
        if (oil < 0)
        {
            start = i;
            oil = line[i];
        }
        else
        {
            oil += line[i];
            if (i - start >= gas.size( ))
            {
                return start;
            }
        }
    }
    return -1;
};

代码2


证明

总存在一个加油站,仅用它的油就足够跑到下一个加油站(否则所有加油站的油量加起来将 不够全程)。把下一个加油站的所有油都提前搬到这个加油站来,并把油已被搬走的加油站无视掉。在剩下的加油站中继续寻找油量足以到达下个加油站的地方,不断合并加油站,直到只剩一个加油站为止。显然从这里出发就能顺利跑完全程。

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

       int start = gas.size()-1;
       int end = 0;
       int sum = gas[start] - cost[start];
       while (start > end)
       {
          if (sum >= 0)
          {
             sum += gas[end] - cost[end];
             ++end;
          }
          else
          {
             --start;
             sum += gas[start] - cost[start];
          }
       }
       return sum >= 0 ? start : -1;

    }
};

网上神作


证明 
先让汽车油箱里装好足够多的油,随便从哪个加油站出发试跑一圈。车每到一个加油站时,记录此时油箱里剩下的油量,然后把那个加油站的油全部装上。试跑完一圈后,检查刚才路上到哪个加油站时剩的油量最少,那么空着油箱从那里出发显然一定能跑完全程。

算法思路为: 
1. 任取一个加油站,作为起点start,然后往前一直开,直到油不够。未能到达之站,记为end。

  1. 此时,说明start不适合作起点站。则偿试以start的前面一站作为起点,并更新start,再次偿试,看能否开到站点end。

  2. 如果不能开到站点end,说明此站也不合适,继续步骤2.

  3. 如果能开到站点end,则继续往前开。直到油不够,并更新end指向此未到之站。继续步骤2.

  4. 重复下去,直到start和end相遇。

在实现在,我将最后一个站点作为start,可以省去一个取模超作,即end前进可以简单的写作++end,而start后退可以简单的写作—start。

省去了写成 end = (end + 1)%N; start =(start + N -1)%N;

class Solution
{
public:
    int canCompleteCircuit(vector<int> &gas, vector<int> &cost) 
    {
        int N = gas.size(), startIndex = -1;
        int sum = 0, total = 0;
        for(int i = 0; i < N; i++)
        {
            sum += (gas[i] - cost[i]);
            total += (gas[i] - cost[i]);

            if(sum < 0)
            {
                startIndex = i; 
                sum = 0;
            }
        }
        return total >= 0 ? startIndex + 1 : -1;
    }
}

此处贴上在网上看到的更好的优雅方案,供大家比较

[cpp]  view plain  copy
  1. public class Solution {  
  2.     public int canCompleteCircuit(int[] gas, int[] cost) {  
  3.         // Note: The Solution object is instantiated only once and is reused by each test case.  
  4.         int N = gas.length, startIndex = -1;  
  5.         int sum = 0, total = 0;  
  6.         for(int i = 0; i < N; i++){  
  7.             sum += (gas[i] - cost[i]);  
  8.             total += (gas[i] - cost[i]);  
  9.             if(sum < 0){  
  10.                 startIndex = i;   
  11.                 sum = 0;  
  12.             }  
  13.         }  
  14.         return total >= 0 ? startIndex + 1 : -1;  
  15.     }  
  16. }  

注,此方案为Java语言实现,故接口与上C++实现略有不同。

其他示例: 
http://blog.chinaunix.net/uid-26456800-id-3456036.html

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: 环形链表的约瑟夫问题是指给定一个环形链表,从链表中的某个节点开始,按照一定规则进行报数并删除节点,直到所有节点都被删除。具体的解决方法是使用一个不带头结点的循环链表来处理。首先构建一个有n个节点的单循环链表,然后从指定的节点开始,按照规定的报数规则进行计数,当计数到达指定数值m时,删除对应的节点。然后从被删除节点的下一个节点重新开始计数,直到最后一个节点被删除,算法结束。遍历环形链表可以通过一个辅助指针curBoy来实现,通过一个while循环遍历链表,直到curBoy.next等于链表的头节点结束。生成一个出圈顺序可以通过创建一个辅助指针helper来实现,首先让helper指针移动m-1次,然后将指向的节点出圈。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* [用环形链表解决约瑟夫问题](https://blog.csdn.net/yunhech/article/details/107444728)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [【数据结构+算法】环形链表——约瑟夫环(Josephu)问题](https://blog.csdn.net/m0_45097186/article/details/104227805)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值