👋 欢迎阅读

🎯 欢迎来到「加油站」题解之旅! 本文将带你从“环形路线上能否绕行一圈”这一实际问题出发,深入理解贪心算法的巧妙应用,并掌握如何通过一次遍历快速找到唯一可行的起点。
在开始之前,建议你先:
-
了解题目背景:这是 LeetCode 134 题,给定两个数组
gas和cost,分别表示每个加油站的油量和到下一站所需的油耗,汽车油箱无限,从任一站点出发,判断能否绕环路一周并返回起点,若存在则返回唯一起点编号,否则返回 -1。本质上,这是一个环形数组上的可行性判断问题。 -
明确学习目标:掌握贪心核心结论——若总油量 ∑gas<∑cost∑gas<∑cost,则无论如何都无法绕行,直接返回 -1;否则一定存在唯一解。并且,在遍历过程中,若从某个起点出发,累计剩余油量在某个位置变为负数,则该起点到当前位置之间的任何点都不能作为起点,下一个起点直接跳到
i+1。理解这种“断点重置”策略的正确性,并熟练实现一趟遍历的代码。 -
准备好环境:建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如
gas = [1,2,3,4,5], cost = [3,4,5,1,2]输出3)。
本文将从问题转化、贪心策略设计(总油量判断 + 断点重置)、正确性直觉到代码实现,层层递进。即使你对贪心算法还不熟悉,我们也会从“如果总油量不够,肯定不行;如果够,从失败点之后重新开始”这一直觉出发,让你轻松抓住核心思想——累计油量非负才能继续,一旦为负就跳过当前区间,因为这段区间内的净消耗大于净产出,不可能作为起点。现在,让我们一起在环形公路上找出那个唯一的加油站起点吧! ⛽🛣️
一、题目

二、做题思路
1. 问题分析(前置分析)
在一条环形公路上有 n 个加油站,第 i 个加油站有 gas[i] 升汽油,从 i 开到 i+1 需要消耗 cost[i] 升汽油。油箱容量无限,初始为空。要求找到一个起点,使得从该点出发能按顺序绕行一圈回到原点,若不存在返回 -1。题目保证若有解则唯一。
核心观察:若所有加油站的总汽油量 小于 总消耗量,则无解。否则,必有一个起点可行。贪心剪枝思想:若从某个起点 i 出发在 step 步时累积油量变为负数,则 从 i+1 到 i+step 之间的任何起点都不可能成功,可直接跳过这些位置,从 i+step+1 重新尝试。
2. 贪心策略(核心决策规则)
-
外层循环枚举起点
i(从0到n-1)。 -
内层循环模拟从
i出发,使用变量ret记录当前累积油量,step记录已走的站数。-
每到达一个加油站
index = (i + step) % n,更新ret += gas[index] - cost[index]。 -
若
ret < 0,说明从i出发无法继续前进到下一个加油站,立即退出内层循环。
-
-
退出内层循环后,若
step == n且ret >= 0,表示成功绕行一圈,返回起点i。 -
若失败,则利用剪枝:将
i直接跳转到i + step(因为i到i+step之间的起点均不可行),外层循环的i++会使下一个起点变为i+step+1。 -
若枚举完所有可能的起点仍未成功,返回
-1。
3. 正确性说明(简单版本,结合图二规律)
若从起点 i 出发,在 step 步(即到达第 i+step 个加油站之前)累积油量 ret 首次变为负数,则说明区间 [i, i+step] 的净油量和为负。对于区间内任意子起点 j(i < j <= i+step),从 j 出发到 i+step 的累积油量 = 总区间和减去前半段(从 i 到 j-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 < n; i++)
{
int ret = 0; // 当前剩余油量(从起点 i 出发后的累积油量)
int step = 0; // 已经走过的加油站数量(从 0 开始计数)
// 内层循环:模拟从起点 i 出发,最多走 n 步(走完一圈)
for (; step < n; step++)
{
int index = (i + step) % n; // 当前到达的加油站下标(环形)
// 累加油量:到达 index 后,加 gas[index] - cost[index]
ret = ret + gas[index] - cost[index];
// 如果油量为负,说明无法继续前进到下一个加油站,跳出内层循环
if (ret < 0)
{
break;
}
}
// 如果走完 n 步且剩余油量 >= 0,说明从 i 出发可以绕行一圈
if (ret >= 0)
{
return i;
}
// 剪枝优化:如果从 i 出发在 step 步时失败(即油量不足),
// 那么从 i+1 到 i+step 之间的任何起点出发,也必然会在到达或超过 i+step 之前失败,
// 因为途中的累积油量只会更差(区间和更小),所以可以直接将 i 跳到 i+step。
// 注意:内层循环退出时 step 是第一个使油量 < 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 < 0,b+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]。 -
当在
i处cur[i] < 0,意味着所有前缀cur[0..i]中可能有的为负,但第一次失败处i之前cur均为非负(否则更早失败)。 -
对于任意
k ∈ [start, i],从k到i的和为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,表示从start到i仍有可能,继续。 -
若
cur < 0,由步骤2性质,从start到i的所有起点都不可行,将它们全部排除,更新start = i+1,cur = 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,求能绕行一圈的起始点,代码只需改动哪一部分?
如果你觉得本文对你有所帮助,欢迎:
👍 点赞 / 收藏
👤 关注作者,获取更多题解
💬 留言交流你的疑问或优化思路
祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨
 —— 题解&spm=1001.2101.3001.5002&articleId=163135009&d=1&t=3&u=4d8a5bc155c04e0195a3987f5cccd12f)

被折叠的 条评论
为什么被折叠?



