134.加油站

目标

在一条环路上有 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 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

说明:

  • gas.length == n
  • cost.length == n
  • 1 <= n <= 10^5
  • 0 <= gas[i], cost[i] <= 10^4

思路

一条环线上有 n 个加油站,加油站 igas[i] 升汽油,从加油站 i 到达加油站 i + 1 需要消耗汽油 cost[i] 升,假设有一辆油箱无限大的汽车,初始油箱为空,问从哪个加油站出发可以绕环线行驶一周,测试用例保证如果解存在则唯一,如果解不存在返回 -1

枚举起点,模拟加油与耗油过程,记录剩余汽油,直到完成环线或者无法抵达下一加油站。值得注意的是,如果从起点 a 出发无法到达 b,那么从 (a, b) 之间的任意加油站出发都无法到达 b,因为到达下一站剩余的汽油是大于等于 0 的,如果带着剩余汽油都无法抵达,那么初始汽油为空更无法到达。我们可以将起点设为 b 继续枚举。

最开始写的是两次循环,后来又改成在一个while循环中,时间复杂度为 O(2n)。之所以是 2n 是因为 start 如果为 n - 1 那么需要再判断能否到达 0 ~ n - 1

优化:如果 gasSum >= costSum, 那么解一定存在。注意到 remainder += gas[i] - cost[i] 其实就是在计算 gasSum - costSum,只是中间小于零的时候重置了,我们只需要一个变量 diff 将其保存起来,直接判断 diff 是否大于 0, 没必要再进行后续判断了。

代码


/**
 * @date 2024-04-13 21:02
 */
public class CanCompleteCircuit134 {

    public int canCompleteCircuit_v2(int[] gas, int[] cost) {
        int n = gas.length;
        int remainder = 0;
        int start = 0;
        int diff = 0;
        for (int i = start; i < n; i++) {
            remainder += gas[i] - cost[i];
            if (remainder < 0) {
                start = i + 1;
                diff += remainder;
                remainder = 0;
            }
        }
        diff += remainder;
        return diff < 0 ? -1 : start;
    }

    public int canCompleteCircuit_v1(int[] gas, int[] cost) {
        int start = 0;
        int cur = 0;
        int n = gas.length;
        int remainder = 0;
        while (start < n) {
            int i = cur % n;
            remainder += gas[i] - cost[i];
            if (remainder < 0) {
                start = ++cur;
                remainder = 0;
            } else if ((i + 1) % n == start) {
                return start;
            } else {
                cur++;
            }
        }
        return -1;
    }

    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        int remainder = 0;
        int start = 0;
        // 第一次循环,遇到断点就重新开始
        for (int i = start; i < n; i++) {
            remainder += gas[i] - cost[i];
            if (remainder < 0) {
                start = i + 1;
                remainder = 0;
            }
        }
        // 第二次循环,遇到断点说明不可达
        for (int i = 0; i < start; i++) {
            remainder += gas[i] - cost[i];
            if (remainder < 0) {
                return -1;
            }
        }
        return start;
    }

}

性能

两次循环

写成一次循环,实际上是循环了两个变量,并且循环中的操作也翻倍了

一次循环