1. 问题描述:
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
来源:https://www.acwing.com/problem/content/description/1090/
2. 思路分析:
这道题目其实有很多种做法,由题目的数据范围可以知道我们不能够使用O(n ^ 2)的算法来解决,下面使用单调队列优化来的思路解决这个题目;首先这道题目是一个环,对于环形的题目我们知道有一个通用的技巧是将环形转化为链来处理,具体的做法是直接复制一遍原数组并且将其添加到原数组的后面,对于任意一个长度为n站点围成的一圈都可以在长度为2n的链中找到答案;因为求解的是由当前的站点是否能够完整周游一周,所以我们先预处理一下题目中的数据,将每一个位置的权值置为当前站点的油量减去这个站点到下一个站点的距离,并且我们可以预处理每一个位置权值相加的结果,也即计算以起点开始的每一段的前缀和,方面后面处理;我们要求解的是能否从一个起点出发到达终点等价于所有的前缀和都是大于0的,也等价于前缀和的最小值是大于等于0的,这样就可以转化为长度为n的单调队列问题,也即求解长度在n之内的区间最值问题。反正求解区间长度的最值需要想到的是单调队列。因为这道题目可以从顺时钟走一圈也可以从逆时钟走一圈,并且这两个方向在处理的时候有些细节是不一样的,所以需要做两遍单调队列优化。其实这道题目是135题最大子序列的扩展,135题求解的是一个长度为n的区间最值,这道题目求解的是n个区间长度的最值问题;首先我们可以先求解顺时钟走一圈的过程(在做单调队列优化前已经处理好了前缀和数组了),因为求解的是从当前的第i个站点出发的区间最值是否大于0所以前缀和是以当前这个位置向右边延伸最多长度为n个距离,求解每一段以当前位置i开始的前缀和的最小值,所以可以从右边往前枚举,这样枚举到i这个位置的时候说明单调队列中已经求解出了以这个位置i后面的最多长度为n的区间的前缀和的最小值,这个时候需要判断当前这个位置的前缀和是否小于等于队头元素的前缀和,如果小于等于说明以当前这个位置开始每一段前缀和都是大于等于0的,这个可以自己画一下比较好理解,因为当前的队头元素是最小的,所以以这个位置开始的每一段只有大于等于0最终到队头位置的区间和才有可能是大于等于第i个站点的前缀和的,所以当大于等于的时候是可以周游一圈的;注意顺时钟做的计算前缀和的时候是不包含第i个站点的前缀和的,所以答案是res[i + 1] = 1而不是res[i] = 1,而在逆时钟做一遍的时候是包含第i个站点的前缀和的,需要区别一下(原因是前缀和保存的含义是不一样的),这两个过程可以看成是对称的,画图其实很好理解。
3. 代码如下:
class Solution:
# 这道题目本质上是135题最大子序和的扩展, 这里求解的n段前缀和是否满足要求, 135题求解的是一段
def process(self):
n = int(input())
# oil[i]为第i个站点的油量, dis[i]为第i个站点到下一个站点的距离
oil, dis = [0], [0]
# 下标从1开始存储元素方便后面处理
for i in range(n):
a, b = map(int, input().split())
oil.append(a)
dis.append(b)
# 声明前缀和列表
s = [0] * (2 * n + 2)
# 顺时针求解每一个点到是否可以完整走一圈
for i in range(1, n + 1):
# 因为是复制一遍接在了原数组后面所以第i + n个位置与第i个位置的元素是一样的
s[i] = s[i + n] = oil[i] - dis[i]
# 累加前缀和, 前缀和存储的是这么多段的剩余油量
for i in range(1, 2 * n + 1):
s[i] += s[i - 1]
q = [0] * (2 * n + 2)
hh, tt = 0, 0
# q[0]为哨兵, 因为求解的是以当前站点i顺时钟是否可以绕一周所以可以逆时终计算长度为为n之内的前缀和的最小值判断是否所有长度在n之内的前缀和是否满足条件
q[0] = 2 * n + 1
res = [0] * (n + 1)
for i in range(2 * n, -1, -1):
# 区间长度大于了n说明hh头指针需要往后移动一位
if q[hh] > i + n: hh += 1
# 注意后面的下标是i + 1, 顺时钟做的时候不包含第i个站点的情况
if i < n:
if s[i] <= s[q[hh]]: res[i + 1] = 1
# 单调队列优化过程
while hh <= tt and s[q[tt]] >= s[i]: tt -= 1
tt += 1
q[tt] = i
# 逆时钟做一遍(其实与前面的过程是对称的), 注意第0个距离需要赋值为第n个站点的距离
dis[0] = dis[n]
# 处理前缀和
for i in range(1, n + 1):
# 注意与顺时钟做的时候的区别因为是逆时钟所以需要减去上一个元素的距离
s[i] = s[i + n] = oil[i] - dis[i - 1]
for i in range(1, 2 * n + 1):
s[i] += s[i - 1]
hh, tt = 0, 0
q[0] = 0
for i in range(1, 2 * n + 1):
if q[hh] + n < i: hh += 1
if i > n:
# 逆时钟做的时候包含第i个站点的情况
if s[i] >= s[q[hh]]: res[i - n] = 1
while hh <= tt and s[q[tt]] <= s[i]: tt -= 1
tt += 1
q[tt] = i
for i in range(1, n + 1):
# 只要有一个满足条件说明就是符合要求的
if res[i] == 1: print("TAK")
else: print("NIE")
if __name__ == "__main__":
Solution().process()