【dawn·算法】最低加油次数(C++)

简要说明
(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 xS。此时考虑集合 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);
        }
    }
};

动态规划(待补充)

  • 原则上,作为优化问题,动态规划也可以解决这一问题。由于贪心算法已经很不错地解决了这个问题,因而这里先留个坑。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值