【贪心算法】加油站

发布于:2025-08-14 ⋅ 阅读:(18) ⋅ 点赞:(0)

🌈题目链接:加油站

题目描述:

在一条环路上有 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;
    }


网站公告

今日签到

点亮在社区的每一天
去签到