目录

题目描述

134. 加油站

题意分析

环形公路上有 n 个加油站,在第 i 站可以加到 gas[i] 升油,从第 i 站开到第 i + 1 站要消耗 cost[i] 升油。油箱容量无限,出发时油箱是空的,问能否找到一个出发点顺时针绕行一整圈回到原点,能则返回该出发点下标,不能则返回 -1。

关键约束是「油箱不能为负」:走到任意一站时累计的加油量必须不小于累计的消耗量,中途一旦透支就算失败。换个说法,把每站的净收益记作 diff[i] = gas[i] - cost[i],那么从起点开始的每一段前缀和都必须非负。

题目额外保证:如果存在解,则解唯一。这条保证很有用,意味着不必担心「找到的是不是字典序最小的那个」,只要找出一个可行起点即可。

数组长度可达 $10^5$,说明对每个起点各模拟一圈的 $O(n^2)$ 做法会超时。边界上要留意 n = 1 的情形:只有一站时,gas[0] >= cost[0] 就返回 0,否则返回 -1;另外 gascost 中的元素都非负,但 diff 可正可负。

解法:贪心跳过失败区间

核心思路

把每站的净收益记为 diff[i] = gas[i] - cost[i]。绕行一圈后的总油量与起点无关,因此 total = sum(diff) 若小于 0,一定无解;但 total >= 0 只说明整体够用,还要找到中途不会透支的起点。

维护当前候选起点 start 和从它出发的剩余油量 tank。若扫描到 itank < 0,不仅 start 失败,starti 的所有位置都失败:对任意中间位置 k,从 startk-1 的累计和非负,而从 starti 的累计和为负,两者相减可知从 ki 也为负。因此下一候选可以直接跳到 i+1

循环不变量是:start 之前的候选都已排除,tank 是从 start 到当前位置的净收益,且重置后每个已扫描前缀都非负。每次重置还相当于把 start 放到全局前缀和的新最低点之后。设 P[j] 是从 0 到 j 的前缀和,则绕回任意 j < start 时的油量为 total - P[start-1] + P[j];因为 P[start-1] 是此前最小值且 total >= 0,该值也非负,所以最终候选能走完整圈。

解题步骤

  1. 初始化 total = 0tank = 0start = 0
  2. 遍历每站,将 gas[i] - cost[i] 同时累加到 totaltank
  3. tank < 0,排除 [start, i],令 start = i + 1tank = 0
  4. 扫描结束后,total >= 0 返回 start,否则返回 -1。

例如 gas = [1,2,3,4,5]cost = [3,4,5,1,2],净收益为 [-2,-2,-2,3,3]。前三站分别使候选起点跳到 1、2、3;从 3 开始累计油量始终非负,且总和为 0,因此答案是 3。

代码实现

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int total = 0;
        int tank = 0;
        int start = 0;

        for (int i = 0; i < gas.length; i++) {
            int diff = gas[i] - cost[i];
            total += diff;
            tank += diff;
            if (tank < 0) {
                // 当前起点到 i 之间的所有位置都无法作为起点。
                start = i + 1;
                tank = 0;
            }
        }
        return total >= 0 ? start : -1;
    }
}
func canCompleteCircuit(gas []int, cost []int) int {
    total := 0
    tank := 0
    start := 0

    for i := 0; i < len(gas); i++ {
        diff := gas[i] - cost[i]
        total += diff
        tank += diff
        if tank < 0 {
            // 失败区间整体跳过,下一个位置才可能成为新起点。
            start = i + 1
            tank = 0
        }
    }
    if total >= 0 {
        return start
    }
    return -1
}

复杂度分析

  • 时间复杂度:$O(n)$,每个加油站只处理一次。
  • 空间复杂度:$O(1)$,只维护三个标量。

关键点总结

  • total 判断是否有解,tank 判断当前候选是否仍可行,两者职责不同。
  • 一次透支可以排除整段候选,这是线性贪心成立的关键证明。
  • 透支条件是严格小于 0;油量恰好为 0 仍然合法。
  • 面试口述顺序:先给总量必要条件,再证明失败区间可整体跳过,最后说明总量非负时剩余候选成立。

易错点总结

  • 只看最后一段的 tank 会把无解误判为有解;gas=[2,3,4]cost=[3,4,3]total=-1,应返回 -1。
  • 透支后将 start 设为 i 会保留已经失败的位置,正确的新起点是 i+1
  • 重置 tank 时不能重置 total,否则会丢失整圈是否可行的信息。
  • 使用 tank <= 0 会误排除油量恰好为 0 的合法状态;gas=[1]cost=[1] 应返回 0。

相似题目

题目 难度 考察点
55. 跳跃游戏 中等 同为可行性贪心,但维护的是「能到达的最远位置」而非累计余量
45. 跳跃游戏 II 中等 从判断可行升级为求最少步数,需要按层维护当前跳跃的边界
122. 买卖股票的最佳时机 II 中等 同样先转成差值数组,但目标是把所有正差值累加而非寻找起点
42. 接雨水 困难 同为一次扫描中维护前缀极值的贪心,难点在左右两侧同时约束
11. 盛最多水的容器 中等 贪心的合法性同样靠「移动短板不会错过最优解」的反证来支撑