LeetCode 第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 - 5 + 4 = 4 升汽油
你已经走完了一圈,返回 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
解题思路
方法一:暴力法
最直观的方法是尝试从每个加油站出发,模拟一次环路行驶,检查是否能够回到起点。
关键点:
- 从每个加油站出发,模拟一次环路行驶
- 如果在某个点油量不足,则该起点不可行
- 如果能够回到起点,则找到了解
具体步骤:
- 遍历每个加油站作为起点
- 从起点开始,模拟环路行驶一周
- 如果在某个点油量不足,则该起点不可行,尝试下一个起点
- 如果能够回到起点,则返回起点索引
- 如果所有起点都不可行,则返回-1
时间复杂度:O(n²),其中n是加油站的数量。需要尝试n个起点,每次模拟需要O(n)的时间。
空间复杂度:O(1),只需要常数级别的额外空间。
方法二:一次遍历
我们可以通过一些数学推导,得到一个更高效的解法。
关键点:
- 如果总油量小于总消耗量,则无解
- 如果总油量大于等于总消耗量,则一定有解
- 如果从起点i出发,到达j时油量不足,则i到j之间的任何一点都不可能作为起点
具体步骤:
- 计算总油量和总消耗量,如果总油量小于总消耗量,则返回-1
- 初始化起点start = 0,当前油量curGas = 0
- 遍历所有加油站:
- 更新当前油量:curGas += gas[i] - cost[i]
- 如果当前油量小于0,则将起点更新为下一个加油站,并将当前油量重置为0
- 返回起点start
时间复杂度:O(n),其中n是加油站的数量。只需要遍历一次加油站。
空间复杂度:O(1),只需要常数级别的额外空间。
图解思路
暴力法分析表
以示例1为例:gas = [1,2,3,4,5], cost = [3,4,5,1,2]
起点 | 模拟过程 | 结果 |
---|---|---|
0 | 起点油量: 0 + 1 = 1 到达1: 1 - 3 = -2 (油量不足) | 不可行 |
1 | 起点油量: 0 + 2 = 2 到达2: 2 - 4 = -2 (油量不足) | 不可行 |
2 | 起点油量: 0 + 3 = 3 到达3: 3 - 5 = -2 (油量不足) | 不可行 |
3 | 起点油量: 0 + 4 = 4 到达4: 4 - 1 = 3, 加油: 3 + 5 = 8 到达0: 8 - 2 = 6, 加油: 6 + 1 = 7 到达1: 7 - 3 = 4, 加油: 4 + 2 = 6 到达2: 6 - 4 = 2, 加油: 2 + 3 = 5 回到3: 5 - 5 = 0 (成功回到起点) | 可行 |
4 | 起点油量: 0 + 5 = 5 到达0: 5 - 2 = 3, 加油: 3 + 1 = 4 到达1: 4 - 3 = 1, 加油: 1 + 2 = 3 到达2: 3 - 4 = -1 (油量不足) | 不可行 |
一次遍历分析表
以示例1为例:gas = [1,2,3,4,5], cost = [3,4,5,1,2]
索引 | gas[i] | cost[i] | gas[i] - cost[i] | 当前油量 | 起点 | 说明 |
---|---|---|---|---|---|---|
初始状态 | - | - | - | 0 | 0 | 初始起点为0 |
0 | 1 | 3 | -2 | -2 | 1 | 油量不足,更新起点为1 |
1 | 2 | 4 | -2 | -2 | 2 | 油量不足,更新起点为2 |
2 | 3 | 5 | -2 | -2 | 3 | 油量不足,更新起点为3 |
3 | 4 | 1 | 3 | 3 | 3 | 油量充足,保持起点不变 |
4 | 5 | 2 | 3 | 6 | 3 | 油量充足,保持起点不变 |
总油量 = 1 + 2 + 3 + 4 + 5 = 15
总消耗量 = 3 + 4 + 5 + 1 + 2 = 15
总油量 >= 总消耗量,有解,起点为3
代码实现
C# 实现
public class Solution {
public int CanCompleteCircuit(int[] gas, int[] cost) {
int n = gas.Length;
int totalGas = 0;
int totalCost = 0;
int curGas = 0;
int start = 0;
for (int i = 0; i < n; i++) {
totalGas += gas[i];
totalCost += cost[i];
curGas += gas[i] - cost[i];
if (curGas < 0) {
start = i + 1;
curGas = 0;
}
}
return totalGas >= totalCost ? start : -1;
}
}
Python 实现
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
n = len(gas)
total_gas = 0
total_cost = 0
cur_gas = 0
start = 0
for i in range(n):
total_gas += gas[i]
total_cost += cost[i]
cur_gas += gas[i] - cost[i]
if cur_gas < 0:
start = i + 1
cur_gas = 0
return start if total_gas >= total_cost else -1
C++ 实现
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size();
int totalGas = 0;
int totalCost = 0;
int curGas = 0;
int start = 0;
for (int i = 0; i < n; i++) {
totalGas += gas[i];
totalCost += cost[i];
curGas += gas[i] - cost[i];
if (curGas < 0) {
start = i + 1;
curGas = 0;
}
}
return totalGas >= totalCost ? start : -1;
}
};
执行结果
C# 实现
- 执行用时:132 ms
- 内存消耗:42.9 MB
Python 实现
- 执行用时:52 ms
- 内存消耗:17.9 MB
C++ 实现
- 执行用时:4 ms
- 内存消耗:68.9 MB
性能对比
语言 | 执行用时 | 内存消耗 | 特点 |
---|---|---|---|
C# | 132 ms | 42.9 MB | 执行速度适中,内存消耗适中 |
Python | 52 ms | 17.9 MB | 执行速度适中,内存消耗较低 |
C++ | 4 ms | 68.9 MB | 执行速度最快,内存消耗较高 |
代码亮点
- 🎯 一次遍历解决问题,时间复杂度为O(n)
- 💡 巧妙利用数学推导,避免了暴力解法的高时间复杂度
- 🔍 同时计算总油量和总消耗量,快速判断是否有解
- 🎨 代码简洁清晰,逻辑易于理解
常见错误分析
- 🚫 没有考虑总油量小于总消耗量的情况,导致无解时仍返回起点
- 🚫 更新起点的逻辑错误,导致找不到正确的起点
- 🚫 没有正确处理环路的特性,导致结果错误
- 🚫 使用暴力解法时,没有正确模拟环路行驶,导致结果错误
解法对比
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
暴力法 | O(n²) | O(1) | 思路直观,易于理解 | 时间复杂度高,效率低 |
一次遍历 | O(n) | O(1) | 时间复杂度低,效率高 | 需要一定的数学推导 |
贪心算法(一次遍历的变种) | O(n) | O(1) | 时间复杂度低,效率高 | 实现稍复杂 |