## [$AcWing$ $1088$ 旅行问题](https://www.acwing.com/problem/content/1090/) ### 一、题目描述 $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$ **输入样例**: ```cpp {.line-numbers} 5 3 1 1 2 5 2 0 1 5 4 ``` **输出样例**: ```cpp {.line-numbers} TAK NIE TAK NIE TAK ``` ### 二、题意理解 ![](https://img2022.cnblogs.com/blog/8562/202202/8562-20220223134220627-777278367.png) 给定一个 **环**,**环** 上有 $n$ 个节点,编号从 $1∼n$,以及一辆小车车 每一个 **节点 $i$** 有一个 **权值** $p_i$ 表示当车 **到达该点** 时,可以 **获得** 的 油量 还有一个 **权值** $d_i$ 表示当车从 **节点 $i$** 到 **节点 $i+1$** 所需要 **消耗** 的 油量 现有一辆车想从环上 **任意点** 出发,**顺时针** 或 **逆时针** 绕环一圈走回起点 行驶的过程中,**油量不能为** **负数**,**初始油量** 为 **起点** 处所能获得的 油量 判断能否完成 **环圈行驶** ### 三、暴力做法 看示例: ```cpp {.line-numbers} 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$** ```cpp {.line-numbers} 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$** ```cpp {.line-numbers} 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]$** ,那就肯定是不中了,我也不管你其它节点啥样,反正最小的小于起始值就是表示中间有断油的情况发生。 这句话是后面优化时采用单调队列的基础,因为这就明显指向了 **在区间内找出最小值**! #### 暴力法 ```cpp {.line-numbers} #include 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$ ```cpp {.line-numbers} #include 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; } ```