🌈题目链接:加油站
题目描述:
在一条环路上有
n
个加油站,其中第i
个加油站有汽油gas[i]
升。你有一辆油箱容量无限的的汽车,从第
i
个加油站开往第i+1
个加油站需要消耗汽油cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。给定两个整数数组
gas
和cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回-1
。如果存在解,则 保证 它是 唯一 的。
示例 1:
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2] 输出: 3 解释: 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 因此,3 可为起始索引。示例 2:
输入: gas = [2,3,4], cost = [3,4,3] 输出: -1 解释: 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 因此,无论怎样,你都不可能绕环路行驶一周。
✅暴力解法:
看到这个题目,由于数组是定长的,每一个加油站都可能作为起点,我们只需要暴力遍历每个加油站,再尝试从每个加油站出发,遍历一周,看是否能环绕一圈。如果能,我们就直接返回这个起点即可。小tips,我们可以把cost数组和gas 看成一个diff净收益数组,这样想能清楚点。
代码如下:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
for(int i=0; i<gas.size(); i++)
{
int totalsum = 0;
for(int step=0; step<gas.size(); step++)
{
int nextstep = (i+step) % gas.size();
totalsum += gas[nextstep] - cost[nextstep];
if(totalsum <0)
{
break;
}
}
if(totalsum >= 0)
{
return i;
}
}
return -1;
}
由于n的范围是10^5,暴力时间复杂度为O(N^2),必定超时。
我们来优化一下这个线性的暴力枚举
✅贪心法
我们来思考一下,我们需要挨个加油站一个个遍历吗,能不能舍去一些没必要的加油站。我们把cost和gas数组看成一个净利益数组,如图:
假设从a加油站出发,最多能走到f,走到f就发现再走不了了。此时我们有必要再从b,c,e加油站挨个尝试吗?没必要了!why?
我们可以简单证明一下:
由于a最多能走到f,所以a+b+c+e+f<0,由于a>0(因为a能走到b),所以b+c+e一定是<0
所以从b.c.e出发,也一定不能到f。因为(b,c,e)加一个>0的数(a)都不能走到f,那么(b,c,e)肯定不能到达了。
既然知道了这个道理,我们就可以从一个加油站尝试出发比如start,看他最远能走到哪里就不能走了比如end。那么下一次我们加油站的起点一定是end+1位置。这期间[start,end]的所有点都不能作为起点了!
我们在暴力枚举的基础上简单优化即可:
即更新加油的位置稍做调整:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
for (int i = 0; i < gas.size(); i++) {
int sum = 0, step = 0;
for (; step < gas.size(); step++) {
int currenti = ((step + i) % gas.size()); // 求出下一步
sum += gas[currenti] - cost[currenti];
if (sum < 0) {
break;
}
}
if (sum >= 0)
return i;
i = i + step; // 这之间i<->i+step之间的所有点都不能作为起点
}
return -1;
}
最差情况,我们线性枚举两次,也一定更新出加油站的位置。所以时间复杂度为O(N)
✅换种更优雅的代码。也是利用贪心
观察一:我们用一个变量记所有加油站的和totalsum。那么我们就不难得出一个结论,如果该和<0,那么无论从哪个起点出发,都不能环绕一圈。
观察二:我们只考虑diff净利益数组,那么如果当前加油站的利益currsum小于0,那么当前加油站一定不能作为起点。
观察三:如果从当前加油站出发,能走到终点,并且totalsum还是>0的,那么该加油站就是能环绕一圈的起点(答案唯一),因为该加油站前面的total已经计算了,该加油站再把剩下几站走完整个total还能>0。
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int totalsum = 0, targeti = 0, currsum = 0;
for(int i=0; i<gas.size(); i++)
{
currsum += gas[i] - cost[i];
totalsum += gas[i] - cost[i];
if(currsum < 0)
{
// 不可达下一步
currsum = 0;
targeti = i+1; // 目标起点一定不在i+1之前
}
}
return totalsum >=0 ? targeti : -1;
}