简要说明 (1) 题目来源:LeetCode(871. 最低加油次数) (2) 代码和思路仅供参考。
题目简介
- 汽车从起点出发驶向目的地,该目的地位于出发位置东面 target 英里处。
- 沿途有加油站,每个 station[i] 代表一个加油站,它位于出发位置东面 station[i][0] 英里处,并且有 station[i][1] 升汽油。
- 假设汽车油箱的容量是无限的,其中最初有 startFuel 升燃料。它每行驶 1 英里就会用掉 1 升汽油。
- 当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。
- 为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 -1 。
- 注意:如果汽车到达加油站时剩余燃料为 0,它仍然可以在那里加油。如果汽车到达目的地时剩余燃料为 0,仍然认为它已经到达目的地。
输入输出样例:
输入: target = 100, startFuel = 10, stations = [[10,60],[20,30],[30,30],[60,40]] 输出: 2 说明: 题目说明以 startFuel=10 为初始油量开往目的地 target = 100 处。我们可以在[10,60]加油站先加油,此时油量为(10-10+60) = 60。随后在[60,40]加油站再加一次油,此时油量为(60-50+40) = 50,而60+50 = 110 > 100,说明已经可以到达终点,此时加油次数为2。
输入: target = 100, startFuel = 10, stations = [[10,60], [75,40]] 输出: -1 说明: 即便在第一个加油站加油,最远也只能开到10+60 = 70处。
思路分析
- 显然,这是一道典型的优化问题(Optimal Problem),让我们可以用最少的加油次数到达终点。而优化问题的最有效的方法,就是动态规划和贪心。
- 由于一个明显的原因,大家都更偏好贪心算法,因此此时我们要考虑两个问题,如何贪心,以及是否满足贪心性质。
最初思路(错误)
- 最开始我的思路是,选择从上一个加油站到所能走到的最远处为止,加油量最多的加油站,这个流程显然只需要
O
(
n
)
O(n)
O(n)的复杂度(线性扫描,每次记录最大值)。但这个方案甚至可能产生一个错误的结果。我们考虑下列情况:
输入: target = 100, startFuel = 20, stations = [[10,40], [20,50], [70,20], [80,10]]
- 如果按照之前的思路,最开始可以走到最远的位置为20,此时经过两个加油站[10,40]和[20,50],根据我们的“贪心”原则选择后者。第一次加油后,可走到的最远位置为70,此时在这段路程中只有[70,20]加油站。第二次加油后,可走到的最远位置为90,此时在这段路程中只有[80,10]加油站。第三次加油后,可到达终点。但显然这并不是最优答案,因为可以只加两次油:分别是[10,40]和[20,50],总油量为110。
正确思路以及正确性证明
- 从上面的例子可以看出,最初的思路有问题,但可以在此基础上进行改正。在第一次加油过程后,我们路过的所有加油站是[10,40]和[70,20],在这一基础上再选择油量最多的加油站。虽然在现实情况中我们已经无法再倒回去了,但对于优化问题来说,我们可以先“纸上谈兵”,再“付诸行动”,十分合理。
- 因而我们确定我们的贪心策略:在当前经过的所有未加过油的加油站中,总是选择加油量最大的那个加油站。这就需要一个“队列”来维护这个加油站集合,最大堆呼之欲出,此时时间复杂度就不再是
O
(
n
)
O(n)
O(n)了。
- 贪心性质的证明比较简单,仅给出一个思路:如果在某一次选择中没有按照贪心策略选择加油量最大的加油站
x
x
x,而是选择了另一个加油站
x
′
x'
x′,并且成功找到了一个最小加油站集合
S
S
S,且满足
x
′
∈
S
x'\in S
x′∈S。此时考虑集合
S
′
=
(
S
?
{
x
′
}
)
∪
{
x
}
S' = (S-\lbrace x' \rbrace)\cup \lbrace x\rbrace
S′=(S?{x′})∪{x},可以看出当用
x
x
x替换在那一次选择中的
x
′
x'
x′时,总油量上升,不会干扰之后的选择,因而
S
′
S'
S′也是一个满足要求的最小加油站集合。
代码部分
插入排序解决集合维护
- 在这里,我们使用类似插入排序的思路来维护加油站集合。
int minRefuelStops(int target, int startFuel, vector<vector<int>>& stations) {
if (startFuel >= target)
return 0;
vector<int> station_queue;
int position = startFuel;
int i=0;
int times=0;
int station_number = stations.size();
while (i<station_number && position>=stations[i][0]) {
vector<int>::iterator it;
for (it=station_queue.begin(); it!=station_queue.end(); ++it){
if (stations[i][1] > stations[*it][1])
break;
}
station_queue.insert(it, i);
++i;
}
while (position < target) {
if (station_queue.empty()) {
return -1;
}
position += stations[station_queue[0]][1];
station_queue.erase(station_queue.begin());
++times;
while (i<station_number && position>=stations[i][0]) {
vector<int>::iterator it;
for (it=station_queue.begin(); it!=station_queue.end(); ++it){
if (stations[i][1] > stations[*it][1])
break;
}
station_queue.insert(it, i);
++i;
}
}
return times;
}
- 正确性证明在第二部分已经给出,此时我们考虑复杂度。对于加油站集合调整操作来说,需要进行线性扫描以确定插入位置,每次扫描需要
O
(
n
)
O(n)
O(n)时间。一共要考虑
n
n
n个加油站,因而其复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
最大堆
- 如同我们此前所说的,“每次选取加油量最大的加油站”这一贪心策略,几乎是为最大堆这一数据结构量身定做的。因而,结合对数据结构的复习,写一个最大堆版本的解决方案。
int minRefuelStops(int target, int startFuel, vector<vector<int>>& stations) {
if (startFuel >= target)
return 0;
maxHeap maxheap;
int position = startFuel;
int i=0;
int times=0;
int station_number = stations.size();
while (i<station_number && position>=stations[i][0]) {
maxheap.insert(i, stations);
++i;
}
while (position < target) {
if (maxheap.empty()) {
return -1;
}
position += stations[maxheap.extract(stations)][1];
++times;
while (i<station_number && position>=stations[i][0]) {
maxheap.insert(i, stations);
++i;
}
}
return times;
}
- 此时,只需要将维护原先集合的操作等价地换成最大堆的插入(insert)和提取(extract)操作。此时,每个insert和extract操作的时间复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn),则总的时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),比起使用类似插入排序的维护方式来说,时间复杂度更低了。
- 对于最大堆数据结构可作如下定义。自己在写时忽略了-1/2=0,从而调了大半天百思不得其解,所幸最后还是找到问题了,也算是对数据结构和算法设计的一次复习吧。
class maxHeap {
public:
vector<int> heap;
bool empty() {
return heap.empty();
}
void insert(int i, vector<vector<int>>& stations) {
heap.push_back(i);
up_heapify(heap.size()-1, stations);
}
int extract(vector<vector<int>>& stations) {
if (heap.size() == 0)
return -1;
int ret = heap[0];
if (heap.size() == 1) {
heap.pop_back();
}
else {
heap[0] = heap[heap.size()-1];
heap.pop_back();
down_heapify(0, stations);
}
return ret;
}
void up_heapify(int i, vector<vector<int>>& stations) {
int p = (i-1)/2;
int temp = heap[i];
while (i>0 && stations[heap[p]][1] < stations[temp][1]) {
heap[i] = heap[p];
i = p;
p = (i-1)/2;
}
heap[i] = temp;
}
void down_heapify(int i, vector<vector<int>>& stations) {
int child = 2*i+1;
int size = heap.size();
int temp;
if (child+1<size && stations[heap[child]][1] < stations[heap[child+1]][1])
child++;
if (child<size && stations[heap[i]][1] < stations[heap[child]][1]) {
temp = heap[child];
heap[child] = heap[i];
heap[i] = temp;
down_heapify(child, stations);
}
}
};
动态规划(待补充)
- 原则上,作为优化问题,动态规划也可以解决这一问题。由于贪心算法已经很不错地解决了这个问题,因而这里先留个坑。
|