题目描述:
John 打算驾驶一辆汽车周游一个环形公路。
公路上总共有 n 个车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。
John 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。
在一开始的时候,汽车内油量为零,John 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。
任务:判断以每个车站为起点能否按条件成功周游一周。
输入格式
第一行是一个整数 n,表示环形公路上的车站数;
接下来 n 行,每行两个整数 pi,di,分别表示表示第 i 号车站的存油量和第 i 号车站到下一站的距离。
输出格式
输出共 n 行,如果从第 i 号车站出发,一直按顺时针(或逆时针)方向行驶,能够成功周游一圈,则在第 i 行输出 TAK,否则输出 NIE。
数据范围
3≤n≤10^6,
0≤pi≤2×10^9,
0<di≤2×10^9
输入样例:
5
3 1
1 2
5 2
0 1
5 4
输出样例:
TAK
NIE
TAK
NIE
TAK
分析:
这题是y总的思路随便听听,然后就按照自己思路写了,代码可能与y总思路有所出入。
首先,既然是环形问题,自然按照处理环形问题的一般思路,展开成两倍长度的线性序列。比如n = 5,就可以展开成1,2,3,4,5,1,2,3,4,5,这些序列的编号从1到10,则求从1顺时针出发能否回到1,就是求从编号为1的点出发能否到达编号为6的点,求从5逆时针出发能否回到5,就是求从编号为10的点出发能否到达编号为5的点。
能否到达取决于剩余油量是否够。p[i]表示在第i个加油站可以加的油量,d[i]表示从第i个点走到第i + 1个点的路程,则p[i] - d[i]就表示从第i个点到第i+1个点油量的增减情况,先考虑顺时针的情况,s[i] = s[i-1] + p[i] - d[i]表示剩余油量的前缀和,也就是说s[i]表示刚走到第i + 1个点还未加油时汽车的剩余油量,如果油量是负数,就说明不够到达。当然,在代码中的含义与这里有所不同,因为我们要求从编号为1的点到编号为2n的点的前缀和。我们需不需要每到一个站就判断下剩余油量呢,显然不需要这样做,只需要回到起点时回顾下全程的最低油量是否小于0即可。从1出发,顺时针走到n,我们需要求s[1]-s[0],s[2]-s[0],...,s[n]-s[0]中的最小值,也就是说,求一个长度为n的区间的最小值,所以可以使用单调队列优化。
for(int i = 1;i <= 2 * n;i++) s[i] = p[i] - d[i] + s[i - 1];//顺时针
int hh = 0,tt = 0;
for(int i = 1;i <= 2 * n - 1;i++){
if(i - q[hh] >= n) hh++;
while(hh <= tt && s[q[tt]] >= s[i]) tt--;
q[++tt] = i;
if(i >= n && s[q[hh]] - s[i - n] >= 0) ans[i - n + 1] = true;
}
首先观察下顺时针的单调队列代码,由于遍历到s[n]时表示走到了第i + 1个点剩余的油量,所以i大于等于n时就可以统计区间长度为n的最小值了,意味着i=n + 1时,就到达了从2顺时针出发的终点,i = 1就不应该出现在队列里了,所以i - q[hh] >= n时出队头,我们要找的是区间中的最小值,所以队头元素需要最小,因此当s[i]小于等于队尾元素时,需要出队尾。正如上面所说,i = n时,实际上是到达了从1顺时针出发的终点,我们将这个区间中s的最小值s[q[hh]]减去起点前的前缀和s[i-n]就表示这段区间的最低油量了,只要它不是负数,就说明从这个区间的起点出发可以到达终点,终点是i + 1,起点就是i - n + 1,。
下面考虑逆时针的代码,你甚至需要倒着枚举序列,因此需要后缀和。
s[2 * n + 1] = 0;//逆时针
for(int i = 2* n;i >= 1;i--) s[i] = p[i] - d[i - 1] + s[i + 1];
hh = 0,tt = 0,q[tt] = 2 * n + 1;
for(int i = 2 * n;i >= 2;i--){
if(q[hh] - i >= n) hh++;
while(hh <= tt && s[q[tt]] >= s[i]) tt--;
q[++tt] = i;
if(i <= n + 1 && !ans[i - 1] && s[q[hh]] - s[i + n] >= 0) ans[i - 1] = true;
}
第一个要考虑的起点是编号为2n的点,也就是原序列中编号为n的点,这里的s[i]同样表示从第i个点出发逆时针到达下一个加油站的剩余油量,此时的s[i] = s[i+1] + p[i] - d[i-1],因为第i个加油站能加的油量是p[i],而从第i个点到达第i-1个点的距离是d[i-1]。逆时针的单调队列的写法与顺时针的基本一致,同样用特殊的例子改下边界即可。从2n出发,当i = n + 1也就是到达了终点n,一旦队列的队头减去i达到了n,就需要出队头,出队尾的操作与顺时针的一样,都是找区间的最小值,第一个终点是n,此时i = n + 1,所以i = n + 1后开始统计最小值,此时区间的起点恰好是i - 1,再判断下这段区间的最小值是否小于0即可。
总的代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2000005;
typedef long long ll;
int p[N],d[N],q[N];
ll s[N];
bool ans[N];
int main(){
int n;
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] = p[i] - d[i] + s[i - 1];//顺时针
int hh = 0,tt = 0;
for(int i = 1;i <= 2 * n - 1;i++){
if(i - q[hh] >= n) hh++;
while(hh <= tt && s[q[tt]] >= s[i]) tt--;
q[++tt] = i;
if(i >= n && s[q[hh]] - s[i - n] >= 0) ans[i - n + 1] = true;
}
s[2 * n + 1] = 0;//逆时针
for(int i = 2* n;i >= 1;i--) s[i] = p[i] - d[i - 1] + s[i + 1];
hh = 0,tt = 0,q[tt] = 2 * n + 1;
for(int i = 2 * n;i >= 2;i--){
if(q[hh] - i >= n) hh++;
while(hh <= tt && s[q[tt]] >= s[i]) tt--;
q[++tt] = i;
if(i <= n + 1 && !ans[i - 1] && s[q[hh]] - s[i + n] >= 0) ans[i - 1] = true;
}
for(int i = 1;i <= n;i++){
if(ans[i]) printf("TAK\n");
else printf("NIE\n");
}
return 0;
}