LeetCode 134 加油站 全面详细题解【持续更新所有解法】

题目来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/gas-station
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一起来读题(中英对照)

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

There are n gas stations along a circular route, where the amount of gas at the ith station is gas[i].
(解析:there be sth 句型表示某个地方有某个东西,这是我们都知道的。那说点不是所有人都知道的 [赶紧说啊你废话这多 ],其实在英语中 be 可以理解为 live ,即 be 可以代表存在的意思,这在某些大家可能不太理解的 there be 句型中有关键作用。along 介词,表示沿着的意思,a circular route 一个圆形路线或者一个环形路线,where 后面的宾语从句解释了 route 路线上的情况,at the ith 在第i个加油站上,the amount of gas 汽油量是 gas[i])

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

You have a car with an unlimited gas tank and it costs cost[i] of gas to travel from the ith station to its next (i + 1)th station. You begin the journey with an empty tank at one of the gas stations.
(解析:car with … 这里的 with 介词理解为带有某种状态,即一量有着 … 状态的车,tank 坦克,箱子,这里 gas tank 显然是汽油箱;这里 and 连接两个句子,前面是 you have a car ,你有一个无限汽油容量的车,后面是 it costs … to do sth ,即从第 i 站到下一站第 i + 1 站消耗 cost[i] 的汽油。you begin … with… 这里的 with 再次表示带有某种状态)

给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

Given two integer arrays gas and cost, return the starting gas station’s index if you can travel around the circuit once in the clockwise direction, otherwise return -1. If there exists a solution, it is guaranteed to be unique
(解析:arrays 数组,travel around 走一圈,同义词traverse也有遍历的意思,clockwise direction,clock 时钟,很明显这个单词意思是顺时针方向。unique 很常见了,唯一的,即答案保证唯一)

解法一 单调队列 O(n)

【解法来自 ACWING 题号 1088】

第一步:破环成链,把环形通过数组重复一遍,出去环形结构,变成链的结构(更多是方便画图,方便理解,其实不用这样也行)
第二步:使用一个单调队列保存状态,我们要求能否走完一个环,等价问题是这个环上的每个节点存油量大于等于0。每个节点的存油量大于等于0等价于存油量最低的点油量大于等于0。

那我们遍历一次我们破环成链后的数组,向后(正序)向前(逆序)都可以。

第三步:因为要走一圈,所以只有当 n 个点的状态我们已经保存到单调队列中,才可以进行后续判断。所以不论正序逆序,都要再第二次遍历时才能进行判断。
第四步:每次判断,由于我们第一次遍历保存了所有点的状态, 第二次遍历的过程中判断最小值是否大于等于0时,某些点不能使用,因为他可能使得我们的路径大于一圈,例如假如单调队列第一个最小值(维护最小值还是最大值根据我们遍历顺序而定,这里逆序遍历,我们维护的是最小值)是 index=0 的节点,那我们判断 index=2 的点出发能否走一圈时,显然不能使用单调队列头部的值。假如我们判断 index=i 的点能否顺时针走一圈,那么从 index=i 开始,一直到 index=i+n-1 ,总共 n 个点,走完了一圈,所以我们单调队列头部的值的 index 取值范围是 [i + n - 1 % n, i] 或者 [i, i + n - 1]。因为我们遍历是有顺序的,当 index 不满足条件直接 pop 扔掉使用后面的值即可。
第五步:每次判断,我们使用前缀和快速计算某个范围的和,即从 index=i 的点走到单调队列头部 index=x 的那个点时,汽油存量。然后判断它是否大于 0 即可。

Q1:单调队列保存的是什么状态?
A:显然我们不能直接保存汽油存量,因为从哪个点触发不确定,那么该点的存量也不能直接求的,如果可以直接求的,显然我们也不必这么麻烦。还记得我们提到,使用前缀和快速求和,我们虽然不能直接保存存量,但是我们可以保存破环成链之后,某一点的前缀和,在这个线性结构中就可以通过前缀和做差快速的求出从当前需要判断的点 index=i 出发,到单调队列 index=x 的点时剩余的汽油量。然后就可以判断这个最小值是否大于等于0。

class Solution {
public:
    #define X_TYPE int
    
    const int N = 1e5 + 10;

    typedef long long LL;

    vector<int> gas = vector<int> (N, 0);
    vector<int> cost = vector<int> (N, 0);
    vector<X_TYPE> s = vector<X_TYPE> (N << 1, 0);
    vector<X_TYPE> que = vector<X_TYPE> (N << 1, 0);

    int canCompleteCircuit(vector<int>& _gas, vector<int>& _cost) {
        int n = _gas.size();
        int hh = 0, tt = -1;
        int ans = -1;
        
        // vector ans = vector (n, false);

        for (int i = 1; i <= n; ++ i) {
            gas[i] = _gas[i - 1];
            cost[i] = _cost[i - 1];
            s[i] = s[i + n] = gas[i] - cost[i];
        }

        for (int i = 1; i <= n << 1; ++ i) s[i] += s[i - 1];

        for (int i = n << 1; i; i --) {
            while (hh <= tt && que[hh] > i + n - 1) hh ++;
            while (hh <= tt && s[que[tt]] >= s[i]) tt --;
            que[++ tt] = i;

            if (i <= n && s[que[hh]] - s[i - 1] >= 0) {
                ans = i - 1;
                break;
            }
        }

        return ans;
    }
};

解法二 枚举 + 优化 O(n)

简单的枚举每个点能否走一圈,当起点 i 到达 起点 j 之后,下一站无法到达 j + 1,那么 [i, j] 之间任意一点 k 都无法到达 j + 1。因为从 i 开始,到达 k,此时剩余油量大于等于0(不加 k 点的油量),但这种情况下都无法到达 j + 1。如果让 k 作为起点,那么意味着此时只有 k 点的油,那么肯定是无法到达 j + 1 的。
从 i 到 k,k 点时的油量 = 前面的剩余油量(>= 0)+ k 点的油量。无法到达 j + 1。
k 作为起点,k 点时的油量 = k 点的油量,显然更不可能到达 j + 1。
所以下次枚举直接从 j + 1 开始即可。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int n = gas.size();
        for (int i = 0, j; i < n; ) {
        	// 枚举每个点 i
            int left = 0;
            for (j = 0; j < n; ++ j) {
            	// 枚举从 i 开始向前走
                int k = (i + j) % n;
                // 补油再尝试向前走
                left += gas[k] - cost[k];
                if (left < 0) break;
            }
            // 等于 n 说明走完了 n 个点
            if (j == n) return i;
            // 否则 i 到 i + j 都不行,只能从 i + j + 1 枚举
            i = i + j + 1;
        }
        return -1;
    }
};

你可能感兴趣的:(Leetcode,算法,力扣,leetcode,面试,c++)