You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 个节点,编号从 1n,以及一辆小车车

每一个 节点 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;
}