加油站 图解题解
这道题到底在问什么
- 输入
- gas =[4,5,2,6,1,3,7,2,5,4,8,3] cost=[6,3,5,2,7,2,1,8,3,2,4,6]
- 输出
- 5 (从第 5 站出发,能绕完整圈)
最优解:为什么这么做
一句话答案:LeetCode 134 加油站:环形站里求能绕一圈的出发点,贪心一遍扫描——总净油量非负才有解,累加途中油箱一跌成负数就把起点跳到下一站、清零重攒,时间 O(n)、空间 O(1)。
环形加油站,求哪个站能发车绕一圈
n 个加油站围成一圈,第 i 站能加 gas[i] 升油,从第 i 站开到下一站耗 cost[i] 升,油箱起初为空。挑一个站作起点绕一圈,绕回原点就返回它下标,绕不完返回 -1。题面 gas=[4,5,2,6,1,3,7,2,5,4,8,3]、cost=[6,3,5,2,7,2,1,8,3,2,4,6],答案是下标 5。
每个站都试一次当起点,慢在哪
把每个站轮流当起点、模拟开一圈,中途油箱不跌到负就算能绕完。12 个站要试 12 趟、每趟走满一圈,n 个站是 n×n 次操作、也就是 O(n²)。很多起点刚开出去就断油,却还陪着走完才判出局。
总量定生死,跌负点之后才是真起点
先算每站的净油量 net[i]=gas[i]−cost[i]。全部加起来是总净油量,它若为负,说明全程油不够耗、哪都开不完,直接 -1;本题总净油量 +1、非负,可行起点一定存在。
有解后再定它落哪。从某个起点累加 net,油箱非负就往下走;一旦某站油箱跌成负数,说明这个起点到当前站整体是亏的。这中间每一站也当不了起点:从中途某站另起,等于丢掉前面那段净油——那段没让油箱提前跌负、必然非负,丢掉只会更早断油。所以这一段起点全排除,候选直跳到跌负站后一位、清零重攒,起点只前移不回退。
三个变量,一遍扫完就定起点
落到代码就三个变量:total 累加全程净油量、只用来判有无解;tank 是当前起点出发后的油箱;start 记候选起点、从 0 起。遍历每站,total 和 tank 都加这站的 net,一旦 tank 跌成负数,就让 start 跳到 i+1、tank 归零。扫完一趟,total≥0 就返回 start,否则 -1。
题面这 12 个站,start 怎样被逼到 5
净油量依次 −2、+2、−3、+4、−6、+1、+6、−6、+2、+2、+4、−3,加起来 +1、有解。start=0、tank=0 开扫。站 0 加 −2 得 −2 跌负,start 跳到 1、清零。站 1 加 +2 得 2。站 2 加 −3 得 −1 又跌负,start 跳到 3、清零。站 3 加 +4 得 4。站 4 加 −6 得 −2 第三次跌负,start 跳到 5、清零。
从站 5 起没再跌过:站 5 加 +1 得 1,站 6 加 +6 得 7,站 7 加 −6 得 1,站 8 加 +2 得 3,站 9 加 +2 得 5,站 10 加 +4 得 9,站 11 加 −3 得 6。start 停在 5,total 也累到 +1、非负,返回下标 5。
起点是跳不是挪,别踩这几个坑
最坑的是把跳转当逐格挪:tank 跌负时只让 start 挪一格,等于没吸取 start..i 整段都不可行、白试几趟还停在错起点,得一步跳到 i+1。跳完忘清 tank,新起点会背着上段亏账越加越偏。还有拿 tank 判有没有解也错——它被清过零、只反映当前段,得看累到尾的 total。
复杂度上,只从左到右扫一趟、每站常数次运算,时间 O(n),把暴力那圈套圈的 O(n²) 压平;只用三变量,空间 O(1)。
▶ 动画逐步走查(共 27 步)——想跟着动画一帧帧对照就展开
- 3记住这句「一旦从某起点走到某站油箱为负,这之间任何站都当不了起点,起点跳到下一站重新攒」——下面每一帧都在套它。我们把每站的净油量画成一条数轴来扫。
- 4把每站的净油量 net[i]=gas[i]−cost[i] 摆成一排(这就是动画主体,不是加油量本身)。总净油量是 +1,≥0 所以保证有解。绿色高亮的是当前候选起点 start=0,油箱 tank 从 0 开始。
- 5第 1 步:扫到站 0,它的净油量是 -2。把它累加到油箱:tank = 0 -2 = -2。注意——加完油箱变负了,下一帧要处理起点跳转。
- 6油箱变负了!说明从起点 0 一路开到站 0,中途必然在某处断油。关键:0 到 0 之间任何一站都当不了起点(从更靠后出发,少攒了前面的油,只会更早断),所以一次性把它们全排除(灰掉),起点直接跳到下一站 1,tank 归零从头攒。
- 7第 2 步:扫到站 1,它的净油量是 +2。把它累加到油箱:tank = 0 +2 = 2。油箱还没见底,候选起点暂时不变。
- 8油箱 tank=2,还是非负的,说明从起点 1 出发到站 1 一路没断油,这个候选起点先留着,继续往右扫下一站。
- 9第 3 步:扫到站 2,它的净油量是 -3。把它累加到油箱:tank = 2 -3 = -1。注意——加完油箱变负了,下一帧要处理起点跳转。
- 10油箱变负了!说明从起点 1 一路开到站 2,中途必然在某处断油。关键:1 到 2 之间任何一站都当不了起点(从更靠后出发,少攒了前面的油,只会更早断),所以一次性把它们全排除(灰掉),起点直接跳到下一站 3,tank 归零从头攒。
- 11第 4 步:扫到站 3,它的净油量是 +4。把它累加到油箱:tank = 0 +4 = 4。油箱还没见底,候选起点暂时不变。
- 12油箱 tank=4,还是非负的,说明从起点 3 出发到站 3 一路没断油,这个候选起点先留着,继续往右扫下一站。
- 13第 5 步:扫到站 4,它的净油量是 -6。把它累加到油箱:tank = 4 -6 = -2。注意——加完油箱变负了,下一帧要处理起点跳转。
- 14油箱变负了!说明从起点 3 一路开到站 4,中途必然在某处断油。关键:3 到 4 之间任何一站都当不了起点(从更靠后出发,少攒了前面的油,只会更早断),所以一次性把它们全排除(灰掉),起点直接跳到下一站 5,tank 归零从头攒。
- 15第 6 步:扫到站 5,它的净油量是 +1。把它累加到油箱:tank = 0 +1 = 1。油箱还没见底,候选起点暂时不变。
- 16油箱 tank=1,还是非负的,说明从起点 5 出发到站 5 一路没断油,这个候选起点先留着,继续往右扫下一站。
- 17第 7 步:扫到站 6,它的净油量是 +6。把它累加到油箱:tank = 1 +6 = 7。油箱还没见底,候选起点暂时不变。
- 18油箱 tank=7,还是非负的,说明从起点 5 出发到站 6 一路没断油,这个候选起点先留着,继续往右扫下一站。
- 19第 8 步:扫到站 7,它的净油量是 -6。把它累加到油箱:tank = 7 -6 = 1。油箱还没见底,候选起点暂时不变。
- 20油箱 tank=1,还是非负的,说明从起点 5 出发到站 7 一路没断油,这个候选起点先留着,继续往右扫下一站。
- 21第 9 步:扫到站 8,它的净油量是 +2。把它累加到油箱:tank = 1 +2 = 3。油箱还没见底,候选起点暂时不变。
- 22油箱 tank=3,还是非负的,说明从起点 5 出发到站 8 一路没断油,这个候选起点先留着,继续往右扫下一站。
- 23第 10 步:扫到站 9,它的净油量是 +2。把它累加到油箱:tank = 3 +2 = 5。油箱还没见底,候选起点暂时不变。
- 24油箱 tank=5,还是非负的,说明从起点 5 出发到站 9 一路没断油,这个候选起点先留着,继续往右扫下一站。
- 25第 11 步:扫到站 10,它的净油量是 +4。把它累加到油箱:tank = 5 +4 = 9。油箱还没见底,候选起点暂时不变。
- 26油箱 tank=9,还是非负的,说明从起点 5 出发到站 10 一路没断油,这个候选起点先留着,继续往右扫下一站。
- 27第 12 步:扫到站 11,它的净油量是 -3。把它累加到油箱:tank = 9 -3 = 6。油箱还没见底,候选起点暂时不变。
- 28油箱 tank=6,还是非负的,说明从起点 5 出发到站 11 一路没断油,这个候选起点先留着,继续往右扫下一站。
- 29一整趟扫描下来,前面那些会让油箱断油的起点都被一次次跳过排除,最后稳稳留下的 start=5 就是答案。因为总净油量 +1≥0,从它出发保证能绕完一圈。绿色高亮的就是最终出发站。
⚠️ 容易写错的地方
✗ 错:tank<0 时只把 start 往后挪一格
✓ 对:start 直接跳到 i+1
start..i 之间每一站都已被证明当不了起点,逐格挪是白费功夫且会出错
✗ 错:跳起点后忘了把 tank 清零
✓ 对:tank 必须归零
新起点是从 i+1 重新出发,之前的油箱状态作废,不清零会带着旧账算错
✗ 错:用 tank 判有没有解
✓ 对:有没有解要看 total(总净油量)
tank 会被清零、只反映当前段;total 累加全程,total≥0 才有解
完整代码(Python / C++ / Java)
Python
def canCompleteCircuit(gas, cost):
total = 0 # 总净油量,判有没有解
tank = 0 # 当前候选起点出发后的油箱
start = 0 # 当前候选起点
for i in range(len(gas)):
net = gas[i] - cost[i] # 这站净油量
total += net
tank += net
if tank < 0: # 油箱见底
start = i + 1 # 起点跳到下一站
tank = 0 # 清零重新攒
return start if total >= 0 else -1C++
int canCompleteCircuit(vector<int>& gas, vector<int>& cost){
int total = 0, tank = 0, start = 0;
for(int i = 0; i < gas.size(); i++){
int net = gas[i] - cost[i]; // 这站净油量
total += net;
tank += net;
if(tank < 0){ // 油箱见底
start = i + 1; // 起点跳到下一站
tank = 0; // 清零重新攒
}
}
return total >= 0 ? start : -1;
}Java
public int canCompleteCircuit(int[] gas, int[] cost) {
int total = 0, tank = 0, start = 0;
for (int i = 0; i < gas.length; i++) {
int net = gas[i] - cost[i]; // 这站净油量
total += net;
tank += net;
if (tank < 0) { // 油箱见底
start = i + 1; // 起点跳到下一站
tank = 0; // 清零重新攒
}
}
return total >= 0 ? start : -1;
}复杂度
时间
O(n)
只扫一遍 n 个站,每站常数时间累加与判断
空间
O(1)
只用 total / tank / start 三个变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 加油站 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
怎么先一步判断到底有没有可行起点?+
看总净油量 total = Σ(gas[i]−cost[i])。total < 0 表示全程加的油不够耗,从哪出发都开不完,返回 -1;total≥0 则一定存在至少一个可行起点。这一步和找起点是两码事:total 只回答有没有解,具体是哪个站要靠扫描时的 start。
为什么扫一遍留下的 start 一定可行,不会漏掉前面更好的起点?+
前面被跳过的起点都已经被证明会断油、不可行,不存在被冤枉的。而 total≥0 保证至少有一个可行起点,它一定落在最后一次跳转之后那一段里——也就是扫描结束时停住的 start。两点一夹,留下的 start 既可行、又是唯一没被排除掉的候选。
起点为什么能整段跳过,不用从跌负段中间逐个再试?+
假设从起点 a 走到站 i 时油箱首次跌负,那 a 到 i 之间任取一站 b 当新起点,等于丢掉了 a..b−1 那段油。那段既然没让油箱在 b 之前就跌负,它的净油和必然非负;把这份非负的贡献扔掉,只会让油箱更早见底。所以 a..i 整段一起排除、下一个候选直接跳到 i+1。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 加油站 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。