从更本质的角度去看「加油站」问题

题目描述

这是 LeetCode 上的 「134. 加油站」 ,难度为 「中等」

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

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

如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -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 升汽油,正好足够你返回到 3 号加油站。
因此,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 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

基本分析/朴素解法

这是一道比较经典的题目。

估计在 LeetCode 也是有一段时间了,所以连数据范围都没有。

我这里直接规定一下数据范围为 ,这意味着我们不能使用 做法了。

但朴素做法往往是优化的出发点,所以我们还是先分析一下,朴素的做法是怎么样的:

  • 题目要求「合法起点」的下标,因此我们可以枚举所有的「起点」

  • 然后按照「油量 & 成本」模拟一遍,看是否能走完一圈

共有 个「起点」,检查某个「起点」合法性的复杂度是 的。因此整体复杂度为 的。

代码:

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        for (int start = 0; start < n; start++) {
            // 直接跳过第一步都不满足的起点
            if (gas[start] < cost[start]) continue;
            // 剩余油量
            int cur = gas[start] - cost[start];
            // 所在位置
            int idx = (start + 1) % n;
            while (idx != start) {
                cur += gas[idx] - cost[idx];
                // 如果剩余油量为负数,说明无法离开当前位置,走到下一位置
                if (cur < 0break;
                idx = (idx + 1) % n;
            }
            if (idx == start) return start;
        }
        return -1;
    }
}
  • 时间复杂度:
  • 空间复杂度:

PS. 在 LeetCode 上提交了一下,是可以过的 🤣

KMP/DFA

不考虑我们跳过那些第一步就不满足的起点的话,将上述解法中的两个主要逻辑“单独”拎出来看,都是无法优化的:

  • 在没做任何操作之前,我们无法知道哪些起点是不合法的
  • 没有比 更低的复杂度可以验证一个起点的合法性

因此只能使用 解法?

并不是。单独看无法优化,合在一起则可以:随着某个起点「合法性验证」失败,我们可以否决掉一些「必然不合法」的方案。

什么意思呢?

在朴素解法中,当我们验证了某个起点 失败(无法走完一圈)之后,我们接下来会去尝试验证起点

这时候验证 失败过程中产生的“信息”,其实并没有贡献到之后的算法中。

事实上,起点 验证失败,其实是意味存在某个位置 使得「当前油量」为负数。而这个位置 就是验证起点 这过程中产生的“信息”。

它可以产生的价值是:在位置 和位置 之间的所有位置,都不可能是一个合法起点。也就是说,随着起点 验证失败,我们可以否决掉方案不仅仅是位置 ,而是 这些位置。

我们可以证明为什么会有这样的性质:

首先,可以明确的是:因为 gas 数组和 cost 数组是给定的,因此每个位置的「净消耗」是固定的,与从哪个「起点」出发无关。

净消耗是指该位置提供的「油量」和「到达下一位置的油耗」,也就是 gas[i]cast[i] 之间的差值。

证明过程如图:

因此,当有了这个优化之后,我们每个位置其实只会被遍历常数次:当位置 作为「起点」验证失败后,验证过程中遍历过的位置就不需要再作为「起点」被验证了。

代码:

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        for (int start = 0; start < n; ) {
            if (gas[start] < cost[start]) {
                start++;
            } else {
                int cur = gas[start] - cost[start];
                int idx = start + 1;
                while (idx % n != start) {
                    cur += gas[idx % n] - cost[idx % n];
                    if (cur < 0break;
                    idx++;
                }
                if (idx % n == start) return start;
                else start = idx;
            }
        }
        return -1;
    }
}
  • 时间复杂度:
  • 空间复杂度:

总结

看到这里,你可以已经理解了 解法是怎么来的了。

但可能还不清楚这个优化思路的本质是什么,其实这个优化的本质与 KMP 为什么可以做到线性匹配如出一辙。

本质都是利用某次匹配(验证)过程产生的“信息”,来加速之后的匹配(验证)过程(否决掉一些必然合法的方案)。

在我写过的 KMP 题解里,有这么一句原话:

当我们的原串指针从 i 位置后移到 j 位置,不仅仅代表着「原串」下标范围为 的字符与「匹配串」匹配或者不匹配,更是在否决那些以「原串」下标范围为 为「匹配发起点」的子集。

所以,从更本质的角度出发,这道题其实是一道「KMP」思想应用题,或者说广泛性的「DFA」题。

其他

在写「总结」部分的时候,我还特意去看了一下题解区,没有人提到过「KMP」和「DFA」,几乎所有题解都停留在题目标签「贪心算法」的角度去思考。

这是不对的,题目标签的拟定很大程度取决于「写这个标签的人的水平」和「ta 当时看这道题的思考角度」,是一个主观的结果。

学习算法和数据结构,应该是去理解每个算法和数据结构的“某个操作”为什么能够带来优化效果,并将该优化效果的“底层思想”挖掘出来,应用到我们没见过的问题中,这才是真正的“学习”。

最后

这是我们「刷穿 LeetCode」系列文章的第 No.134 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

本文使用 文章同步助手 同步

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355

推荐阅读更多精彩内容