AcWing 1088 旅行问题

\(AcWing\) \(1088\) 旅行问题

一、题目描述

\(John\) 打算驾驶一辆汽车周游一个环形公路。

公路上总共有 \(n\) 个车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。

\(John\) 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。

在一开始的时候,汽车内油量为零,\(John\) 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。

任务:判断以每个车站为起点能否按条件成功周游一周。

输入格式
第一行是一个整数 \(n\),表示环形公路上的车站数;

接下来 \(n\) 行,每行两个整数 \(p_i,d_i\),分别表示表示第 \(i\) 号车站的存油量和第 \(i\) 号车站到 顺时针方向 下一站的距离。

输出格式
输出共 \(n\) 行,如果从第 \(i\) 号车站出发,一直按顺时针(或逆时针)方向行驶,能够成功周游一圈,则在第 \(i\) 行输出 TAK,否则输出 NIE

数据范围
\(3≤n≤10^6,0≤p_i≤2×10^9\),\(0≤d_i≤2×10^9\)

输入样例

5
3 1
1 2
5 2
0 1
5 4

输出样例

TAK
NIE
TAK
NIE
TAK

二、题意理解


给定一个 上有 \(n\) 个节点,编号从 \(1∼n\),以及一辆小车车

每一个 节点 \(i\) 有一个 权值 \(p_i\) 表示当车 到达该点 时,可以 获得 的 油量

还有一个 权值 \(d_i\) 表示当车从 节点 \(i\)节点 \(i+1\) 所需要 消耗 的 油量

现有一辆车想从环上 任意点 出发,顺时针逆时针 绕环一圈走回起点

行驶的过程中,油量不能为 负数初始油量起点 处所能获得的 油量

判断能否完成 环圈行驶

三、暴力做法

看示例:

5
3 1
1 2
5 2
0 1
5 4

用测试用例进行模拟,加快对题目理解:

1、理解题意

\(1->(1)->2->(2)->3->(2)->4->(1)->5 ->(4) ->1\)
\(3~~~~~~~~~~~~~~~~~~~~~~1~~~~~~~~~~~~~~~~~~~~~~ 5~~~~~~~~~~~~~~~~~~~~~~0~~~~~~~~~~~~~~~~~~~~~~5\)

  • 第一行,无\((\ )\)的数字为加油站序号
  • 第一行,\(( \ )\)中为在行走过程中消耗的油量:\(d[i-1]\)
  • 第二行的数字:每个节点可以补充的油量:\(p[i-1]\)

\(Q_1\):为什么是\(p[i-1],d[i-1]\),而不是\(p[i],d[i]\)
:以\(2\)号节点为例,到达了\(2\),还没有加上\(2\)号站点的油之前,此时剩余油量为\(3-1=2,\)\(p[1]-d[1]=3-1=2\)

总结

  • 顺时针到达\(i\)
    • \(d[i-1]\):从\(i-1\)走到\(i\)的石油消耗量
    • \(p[i-1]\):从\(i-1\)点获取到的石油量
  • 逆时针到达\(i\)
    • \(d[i]\):从\(i+1\)走到\(i\)的石油消耗量
    • \(p[i+1]\):从\(i+1\)点获取到的石油量

\(Q_2\):为什么逆时针是\(d[i]\)?按对称来讲,不是应该是\(d[i+1]\)吗?
:这个细节挺有意思,\(d[i]\)中保存的是\(i->i+1\)这段路需要消耗掉的油量,也可以理解为反向从\(i+1->i\)消耗的油量,是一样的。如果写在\(d[i+1]\)就不是这个意思了,表示从\(i+1->i+2\)消耗的油量,细节决定成败啊!

2、前缀和优化

\(s[i]\):从\(1\)号节点出发,到达\(i\)号节点,还未取得\(i\)号节点的油量前,剩余油量

\[\large s[i]=s[i-1]+p[i-1]-d[i-1] \]

其中\(p[i-1]-d[i-1]\)为变化量。

3、【特殊情况】从\(1\)号点出发

模拟从\(1\)号点出发,研究一下在整条路线上,每个节点已到达、但还未加上此节点的油量前,是不是剩余油量全部都大于等于\(0\),如果出现某个节点到达时(未加上本节点的油),油量已经小于\(0\),就意味着不可行,因为 中途没油是不可能跑到下一个节点的,任意时刻需要\(s[i]>=0\)

4、【普通情况】从\(i\)号点出发

办法:【破环成链】

假设从\(i\)点出发,就是在问: \(j ∈[i+1,i+2,i+3, ... ,i+n]\)\(n\)个点中,\(s[j]-s[i]\)是不是一直大于等于\(0\)如果有一个小于\(0\)的就是不合法

\(Q\):为什么要\(s[j]-s[i]\),这是什么意思?
:这本质上和从\(1\)号点出发是一样的,比如从\(3\)号点出发,\(n=10\),就是

\[\large 1,2,\underline{3,4,5,6,7,8,9,10,1,2,3},4,5,6,7,8,9,10 \]

由于我们只计算一遍\(S\)前缀和数组,所以从\(3\)号节点出发,可以认为出发时油量为\(0\),而直接读取\(s[3]\)就不对了,因为它包含了\(a[1],a[2],a[3]\),我们需把它扣除掉,才符合要求,这也就是\(s[j]-s[i]\)的含义了。

5、判断方法

判断\(s[j]-s[i]>=0\)有两种判断办法,分别是:

\((1)\)、计算方法\(I\)


for (int i = 1; i <= n; i++) { //枚举每个出发点
    bool flag = true;
    for (int j = i + 1; j <= i + n; j++)
        if (s[j] - s[i] < 0) {
            flag = false;
            break;
        }
    if (flag) ans[i] = true;
}

\((2)\)、计算方法\(II\)

for (int i = 1; i <= n; i++) { //枚举每个出发点
    LL Min = LLONG_MAX;
    for (int j = i + 1; j <= i + n; j++) Min = min(Min, s[j]); //记录在哪个点是存油量最少的情况
    // s[j]-s[i]>=0 则表示一直保持油量大于等于0
    if (Min >= s[i]) ans[i] = true;
}

其实这两种计算方法本质上是一样的,但第二种更聪明些:

区间内的最小值,也就是 油量最低点,它要是 小于起始值\(s[i]\) ,那就肯定是不中了,我也不管你其它节点啥样,反正最小的小于起始值就是表示中间有断油的情况发生。

这句话是后面优化时采用单调队列的基础,因为这就明显指向了 在区间内找出最小值

暴力法

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2000010;
int n;
int p[N];
int d[N];
LL s[N];
bool ans[N];

int main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; i++) {
        scanf("%d %d", &p[i], &d[i]);
        // 破环成链
        p[i + n] = p[i];
        d[i + n] = d[i];
    }

    // 顺时针,油量增减量的前缀和
    for (int i = 1; i <= 2 * n; i++) s[i] = s[i - 1] + p[i - 1] - d[i - 1];
    for (int i = 1; i <= n; i++) { // 枚举出发点
        LL Min = LLONG_MAX;
        // 找出每个加油站站点到达时的油量最小值,如果最小值都
        for (int j = i + 1; j <= i + n; j++) Min = min(Min, s[j]);
        if (Min >= s[i]) ans[i] = true;
    }

    // 逆时针,油量增减量的后缀和
    // 一正一反跑两回,才能说某个点是不是顺时针、逆时针可以到达全程,跑环成功
    // KAO,前缀和和后缀和一起用,居然不用重新初始化!牛!这个边界s[i+1]=0用的好啊!
    // memset(s, 0, sizeof s);
    for (int i = 2 * n; i; i--) s[i] = s[i + 1] + p[i + 1] - d[i];
    for (int i = n + 1; i <= 2 * n; i++) {
        LL Min = LLONG_MAX;
        for (int j = i - 1; j >= i - n; j--) Min = min(Min, s[j]);
        if (Min >= s[i]) ans[i - n] = true;
    }

    // 枚举输出
    for (int i = 1; i <= n; i++) puts(ans[i] ? "TAK" : "NIE");
    return 0;
}

四、单调队列优化

单调队列 来维护这长度为\(n\)的区间的 前缀和 最小值 时是哪个位置\(j\)

\(Code\)

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
const int N = 2000010; // 破环成链,双倍长度
int n, p[N], d[N];     // n:油站数量,p:加上的油量,d:消耗掉的油量
LL s[N];               // 顺时针:p[i-1]-d[i-1] 的前缀和,逆时针:p[i + 1] - d[i] 的后缀和
int q[N], hh, tt;      // 队列
bool ans[N];           // 结果数组,因为每个站点都有一个结果:是不是能从它出发环行一周,所以,需要一个结果数组

int main() {
    // 此题目数据量 n<=1e6,数据量大,使用scanf进行读取
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d %d", &p[i], &d[i]);
        // 破环成链
        p[i + n] = p[i]; // 在i点的加油量
        d[i + n] = d[i]; // ① 顺时针:从i到下一站i+1的耗油量,②逆时针:从i+1到下一站i的耗油量
    }

    // 一、顺时针
    // (1) 前缀和
    for (int i = 1; i <= n * 2; i++) s[i] = s[i - 1] + p[i - 1] - d[i - 1];

    // 每个节点考查它右侧最长n个长度的窗口中,s[j]的最小值=s[q[hh]]
    // 它右边i+1,i+2,...,i+n需要先入队列,才能让i看到未来,倒序遍历
    // (2) 哨兵
    q[0] = n * 2 + 1;                           // 倒序遍历,添加右侧哨兵
    hh = 0, tt = 0;                             // 单调队列
    for (int i = n * 2; i; i--) {               // 倒序遍历
        while (hh <= tt && q[hh] - i > n) hh++; // 最长n个站点
        /*
        ① 如果最小值都大于s[i],说明i可以环形完成旅行
        ② 走到i时,没加上i站点的油前,考查前i-1,i-2,..,i-n的情况,i还没有参加讨论,所以先用队列解决问题后,再将i入队列
        */
        if (s[q[hh]] >= s[i]) ans[i] = true;       // s[q[hh]]=s[j]区间内最小值,s[j]-s[i]>=0就是可以走到
        while (hh <= tt && s[q[tt]] >= s[i]) tt--; // 准备i入队列,保留年轻+漂亮(数值更小),喜新厌旧,什么东西!
        q[++tt] = i;                               // i入队列
    }

    // 二、逆时针,后缀和
    for (int i = 2 * n; i; i--) s[i] = s[i + 1] + p[i + 1] - d[i]; // 这里有一个细节,d[i]其实就是i+1->i消耗掉的油量
    q[0] = 0;                                                      // 正序遍历,添加左侧哨兵
    hh = 0, tt = 0;                                                // 初始化队列
    for (int i = 1; i <= 2 * n; i++) {                             // 正序遍历
        while (hh <= tt && i - q[hh] > n) hh++;                    // 最长n个站点
        if (s[q[hh]] >= s[i]) ans[i - n] = true;
        while (hh <= tt && s[q[tt]] >= s[i]) tt--;
        q[++tt] = i;
    }

    // 输出
    for (int i = 1; i <= n; i++) puts(ans[i] ? "TAK" : "NIE");
    return 0;
}
posted @ 2022-01-17 11:44  糖豆爸爸  阅读(227)  评论(0编辑  收藏  举报
Live2D