LeetCode 134:加油站(贪心算法) —— 题解

👋 欢迎阅读

🎯 欢迎来到「加油站」题解之旅! 本文将带你从“环形路线上能否绕行一圈”这一实际问题出发,深入理解贪心算法的巧妙应用,并掌握如何通过一次遍历快速找到唯一可行的起点。

在开始之前,建议你先:

  • 了解题目背景:这是 LeetCode 134 题,给定两个数组 gascost,分别表示每个加油站的油量和到下一站所需的油耗,汽车油箱无限,从任一站点出发,判断能否绕环路一周并返回起点,若存在则返回唯一起点编号,否则返回 -1。本质上,这是一个环形数组上的可行性判断问题。

  • 明确学习目标:掌握贪心核心结论——若总油量 ∑gas<∑cost∑gas<∑cost,则无论如何都无法绕行,直接返回 -1;否则一定存在唯一解。并且,在遍历过程中,若从某个起点出发,累计剩余油量在某个位置变为负数,则该起点到当前位置之间的任何点都不能作为起点,下一个起点直接跳到 i+1。理解这种“断点重置”策略的正确性,并熟练实现一趟遍历的代码。

  • 准备好环境:建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 gas = [1,2,3,4,5], cost = [3,4,5,1,2] 输出 3)。

本文将从问题转化、贪心策略设计(总油量判断 + 断点重置)、正确性直觉到代码实现,层层递进。即使你对贪心算法还不熟悉,我们也会从“如果总油量不够,肯定不行;如果够,从失败点之后重新开始”这一直觉出发,让你轻松抓住核心思想——累计油量非负才能继续,一旦为负就跳过当前区间,因为这段区间内的净消耗大于净产出,不可能作为起点。现在,让我们一起在环形公路上找出那个唯一的加油站起点吧! ⛽🛣️


一、题目

134. 加油站 - 力扣(LeetCode)

二、做题思路

1. 问题分析(前置分析)

在一条环形公路上有 n 个加油站,第 i 个加油站有 gas[i] 升汽油,从 i 开到 i+1 需要消耗 cost[i] 升汽油。油箱容量无限,初始为空。要求找到一个起点,使得从该点出发能按顺序绕行一圈回到原点,若不存在返回 -1。题目保证若有解则唯一。

核心观察:若所有加油站的总汽油量 小于 总消耗量,则无解。否则,必有一个起点可行。贪心剪枝思想:若从某个起点 i 出发在 step 步时累积油量变为负数,则 i+1i+step 之间的任何起点都不可能成功,可直接跳过这些位置,从 i+step+1 重新尝试。


2. 贪心策略(核心决策规则)

  • 外层循环枚举起点 i(从 0n-1)。

  • 内层循环模拟从 i 出发,使用变量 ret 记录当前累积油量,step 记录已走的站数。

    • 每到达一个加油站 index = (i + step) % n,更新 ret += gas[index] - cost[index]

    • ret < 0,说明从 i 出发无法继续前进到下一个加油站,立即退出内层循环

  • 退出内层循环后,若 step == nret >= 0,表示成功绕行一圈,返回起点 i

  • 若失败,则利用剪枝:i 直接跳转到 i + step(因为 ii+step 之间的起点均不可行),外层循环的 i++ 会使下一个起点变为 i+step+1

  • 若枚举完所有可能的起点仍未成功,返回 -1


3. 正确性说明(简单版本,结合图二规律)

若从起点 i 出发,在 step 步(即到达第 i+step 个加油站之前)累积油量 ret 首次变为负数,则说明区间 [i, i+step] 的净油量和为负。对于区间内任意子起点 ji < j <= i+step),从 j 出发到 i+step 的累积油量 = 总区间和减去前半段(从 ij-1)的累积油量。由于前半段在失败之前油量始终非负(因为 i 是首次失败点,前面的累积油量 ≥ 0),所以后半段的累积油量 ≤ 总区间和 < 0,必然也会在到达 i+step 之前或之时失败。因此,这些中间起点都可以安全跳过,直接尝试 i+step+1,这保证了算法的时间复杂度为 O(n),同时不会错过可行解。


4. 实现细节(边界防护)

  • 使用变量 ret 记录当前剩余油量,step 记录已走过的加油站数(从 0 开始)。

  • 内层循环中,步数 step 最多到 n,因为最多走一圈。

  • ret < 0 时,立即跳出内层循环,并记录失败的 step 值。

  • 剪枝语句 i = i + step; 使外层循环跳过中间无效起点,注意循环体最后的 i++ 会使实际下一个起点为 i+step+1

  • gas 总和不小于 cost 总和,则一定有解,最终会返回某个起点;否则返回 -1


5. 返回值(目标映射)

返回找到的起点索引 i,若不存在则返回 -1

四、代码

class Solution 
{
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) 
    {
        int n = cost.size();
    // 外层循环:尝试从每个加油站作为起点
    for (int i = 0; i &lt; n; i++) 
    {
        int ret = 0;      // 当前剩余油量(从起点 i 出发后的累积油量)
        int step = 0;     // 已经走过的加油站数量(从 0 开始计数)

        // 内层循环:模拟从起点 i 出发,最多走 n 步(走完一圈)
        for (; step &lt; n; step++) 
        {
            int index = (i + step) % n;   // 当前到达的加油站下标(环形)

            // 累加油量:到达 index 后,加 gas[index] - cost[index]
            ret = ret + gas[index] - cost[index];

            // 如果油量为负,说明无法继续前进到下一个加油站,跳出内层循环
            if (ret &lt; 0) 
            {
                break;
            }
        }

        // 如果走完 n 步且剩余油量 &gt;= 0,说明从 i 出发可以绕行一圈
        if (ret &gt;= 0) 
        {
            return i;
        }

        // 剪枝优化:如果从 i 出发在 step 步时失败(即油量不足),
        // 那么从 i+1 到 i+step 之间的任何起点出发,也必然会在到达或超过 i+step 之前失败,
        // 因为途中的累积油量只会更差(区间和更小),所以可以直接将 i 跳到 i+step。
        // 注意:内层循环退出时 step 是第一个使油量 &lt; 0 的位置(未走满 n 步),
        // 此时 i+step 是失败的位置,下一轮外层循环将从 i+step+1 开始尝试。
        // 但原代码使用 i = i + step; 然后 for 循环自增,相当于跳到 i+step+1,
        // 因为循环体最后 i++ 会执行。
        i = i + step;
    }

    // 所有起点都尝试过,没有可行方案
    return -1;
}
};

五、流程图

六、正确性说明(详细版)

步骤 1:符号与问题建模
+----------------------------------------------------+
|  输入:gas[i](获得油量),cost[i](消耗油量)    |
|  定义 diff[i] = gas[i] - cost[i]                   |
|  从 i 出发,累积油量 cur,经过 j 后               |
|  cur += diff[j],需保持 cur ≥ 0 才能继续。         |
|  目标:找到唯一起点 start 使绕行一圈 cur 始终 ≥0。 |
+----------------------------------------------------+
                    |
                    v
+----------------------------------------------------+
|  贪心策略:从 0 开始模拟,维护当前起点 start 和    |
|  累积油量 cur。若在某个位置 i 时 cur < 0,        |
|  则说明从 start 到 i 之间任意起点都无法绕行,      |
|  直接将 start 设为 i+1,cur 归零,继续。           |
|  剪枝依据(图二):若从 start 出发到 i 失败,      |
|  即 sum_{k=start}^{i} diff[k] < 0,则对于任意     |
|  j ∈ [start, i],从 j 出发到 i 的累积和也必然    |
|  < 0(因为前缀和更小),故跳过这些起点。            |
|  贪心实质:一次遍历,动态调整起点,跳过不可能区间。|
+----------------------------------------------------+
  • 定义 diff[i] = gas[i] - cost[i],表示从 i 到 i+1 的净变化。

  • 若从起点 start 出发,累积油量 cur 小于 0,则失败于 i

  • 图二所示:a+b+c+d+e+f < 0b+c+d+e+f < 0 等,说明任何从 start+1 到 i 开始的子段和也都小于0,因此这些起点不可能成功。


步骤 2:关键性质 —— 剪枝的合理性(前缀和与反证法)
+----------------------------------------------------+
|  性质:若从 start 出发,累积到位置 i 时 cur < 0, |
|  则对于任意 k ∈ [start, i],从 k 出发到达 i 的    |
|  累积油量 sum_{t=k}^{i} diff[t] 也必定 < 0。       |
|  证明:设前缀和 S[x] = sum_{t=start}^{x} diff[t]。 |
|  则从 start 到 i 的累积和为 S[i] < 0。             |
|  对于 k > start,从 k 到 i 的和 = S[i] - S[k-1]。  |
|  由于到达 k 之前累积油量必须 ≥0(否则早已失败),  |
|  即 S[k-1] ≥ 0,因此 S[i] - S[k-1] ≤ S[i] < 0。   |
|  故从 k 出发到 i 也会失败。                         |
|  反证:若存在 k 能成功,则其到达 i 时油量 ≥0,    |
|  则 S[i] - S[k-1] ≥ 0 ⇒ S[i] ≥ S[k-1] ≥ 0,      |
|  与 S[i] < 0 矛盾。                                |
+----------------------------------------------------+

详细论证

  • 设从 start 出发到位置 t 的累积油量为 cur[t],即 cur[t] = sum_{p=start}^{t} diff[p]

  • 当在 icur[i] < 0,意味着所有前缀 cur[0..i] 中可能有的为负,但第一次失败处 i 之前 cur 均为非负(否则更早失败)。

  • 对于任意 k ∈ [start, i],从 ki 的和为 cur[i] - cur[k-1]。由于 cur[k-1] ≥ 0(因为 k-1 < i 且未失败),所以 cur[i] - cur[k-1] < 0因此从 k 出发也会在 i 处失败

  • 反证:若从 k 出发能通过 i,则其累积到 i 的油量 ≥0,即 cur[i] - cur[k-1] ≥ 0,结合 cur[k-1] ≥ 0 可推出 cur[i] ≥ 0,与 cur[i] < 0 矛盾。故剪枝正确。


步骤 3:归纳证明 —— 单次遍历找到唯一解
+----------------------------------------------------+
|  初始:start=0,cur=0,总油量 total=0。            |
|  遍历每个位置 i(0..n-1):                        |
|    total += diff[i],cur += diff[i]。              |
|    若 cur < 0,则根据性质,从 start..i 都无法成功,|
|    重置 start = i+1,cur = 0。                    |
|  遍历结束后,若 total < 0,返回 -1;              |
|  否则 start 即为唯一可行起点。                     |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  归纳假设:每次重置前,所有被跳过的起点都不可能   |
|  成功,且当前 start 是尚未被排除的最小可能起点。   |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  处理位置 i:若 cur 仍 ≥0,则继续;若 cur<0,     |
|  则由性质跳过 [start, i],start 移到 i+1。        |
|  由于总环行条件 total ≥ 0 保证有唯一解,          |
|  最后剩下的 start 即为答案。                      |
+----------------------------------------------------+

详细论证

  • 归纳基础:初始 start=0,尚未排除任何起点。

  • 归纳步骤:假设当前 start 是尚未排除的最小索引,且之前所有起点已被证明不可行。处理到 i 时:

    • cur >= 0,表示从 starti 仍有可能,继续。

    • cur < 0,由步骤2性质,从 starti 的所有起点都不可行,将它们全部排除,更新 start = i+1cur = 0。此时新的 start 仍是尚未排除的最小索引。

  • 终止:遍历结束后,若总油量 total = sum(diff) < 0,则无论如何都无法绕行(因为整个环的净油量为负),返回 -1。否则,由于题目保证解唯一,且所有被跳过的起点都不可能是解,最后剩余的 start 即为唯一可行起点。

🎯 闭幕

动态规划学习路径图

🎉 恭喜你完成了「加油站」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀 动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。

💡 深入思考

  • 本题核心思路是 贪心 + 剪枝:从某个起点出发,如果到达某个位置时油量为负,则说明 起点到该位置之间的所有点都不能作为有效起点,可以直接跳过。为什么可以直接跳过这段区间? 你能从“区间和”的角度解释吗?

  • 代码中 ret 记录累积油量,当 ret < 0 时退出内层循环并跳跃起点。如果某次内层循环走完了 n 步且 ret >= 0,直接返回起点。 为什么此时不需要检查其他起点?

  • 如果 gas 的总和小于 cost 的总和,是否一定无解? 反之,如果总和大于等于,是否一定有解?(提示:这是一个重要性质)

  • 如果 gas[i] - cost[i] 的值有正有负,贪心跳跃起点的依据是什么?你能用一个小例子(如 gas=[2,3,4], cost=[3,4,3])手动模拟代码的跳跃过程吗?

  • 本题空间复杂度 O(1),时间复杂度 O(n)为什么一次遍历就能找到答案? 背后依赖的数学性质是什么?

📚 延伸挑战

  • 将题目改为 如果存在解,返回所有可能的起点(而题目保证唯一),你如何修改代码?

  • 如果汽车的油箱容量 不是无限(比如最大容量为 C),需要模拟油量不能超过 C 的情况,算法需要如何调整?

  • 如果每个加油站 可以多次加油(如绕圈时经过同一加油站可以多次补充),问题性质会改变吗?

  • 如果出发时油箱里 已经有初始油量 startGas,求能绕行一圈的起始点,代码只需改动哪一部分?

如果你觉得本文对你有所帮助,欢迎:

👍 点赞 / 收藏
👤 关注作者,获取更多题解
💬 留言交流你的疑问或优化思路

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值