环形加油站问题

时间:2021-12-08 04:08:57

问题


城市的环形路有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;
}
}

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