[CSP-S2020]贪吃蛇[题解]
[CSP-S2020] 贪吃蛇
题目描述
草原上有 \(n\) 条蛇,编号分别为 \(1, 2, \ldots , n\)。初始时每条蛇有一个体力值 \(a_i\),我们称编号为 \(x\) 的蛇实力比编号为 \(y\) 的蛇强当且仅当它们当前的体力值满足 \(a_x > a_y\),或者 \(a_x = a_y\) 且 \(x > y\)。
接下来这些蛇将进行决斗,决斗将持续若干轮,每一轮实力最强的蛇拥有选择权,可以选择吃或者不吃掉实力最弱的蛇:
- 如果选择吃,那么实力最强的蛇的体力值将减去实力最弱的蛇的体力值,实力最弱的蛇被吃掉,退出接下来的决斗。之后开始下一轮决斗。
- 如果选择不吃,决斗立刻结束。
每条蛇希望在自己不被吃的前提下在决斗中尽可能多吃别的蛇(显然,蛇不会选择吃自己)。
现在假设每条蛇都足够聪明,请你求出决斗结束后会剩几条蛇。
本题有多组数据,对于第一组数据,每条蛇体力会全部由输入给出,之后的每一组数据,会相对于上一组的数据,修改一部分蛇的体力作为新的输入。
数据范围
对于 \(20 \%\) 的数据,\(n = 3\)。
对于 \(40 \%\) 的数据,\(n \le 10\)。
对于 \(55 \%\) 的数据,\(n \le 2000\)。
对于 \(70\%\) 的数据,\(n \le 5 \times {10}^4\)。
对于 \(100\%\) 的数据:\(3 \le n \le {10}^6\),\(1 \le T \le 10\),\(0 \le k \le {10}^5\),\(0 \le a_i, y \le 10^9\)。保证每组数据(包括所有修改完成后的)的 \(a_i\) 以不降顺序排列。
分析
先考虑稍微暴力一点的做法。
根据题目描述,一个蛇如果如果当前轮次选择不吃,当且仅当它吃掉这只蛇后会导致自己在之后的轮次中被吃掉。假设我们先抛去限制条件,模拟整个过程直到只剩最后一只蛇,显然,对于每只蛇,我们可以处理出其于第几个轮次中被吃掉。
这个信息有什么作用呢?考虑一个递归的过程,当我们回溯时,显然已经得到了所有蛇的轮次信息,我们再维护一个值 \(t\),表示到目前为止,游戏在第 \(t\) 轮结束,\(t\) 的初始值为 \(n\)。假设根据得到的信息,当前回溯到的蛇在之后会被吃掉,并且他被吃掉的轮次比 \(t\) 小,说明这条蛇会选择在当前轮次结束游戏,我们更新 \(t\),同时清空当前蛇和被吃掉的蛇的标记,因为它们已经不会再被吃掉了。
这个过程可以用 \(set\) 维护,时间复杂度 \(O(Tnlogn)\),可以拿到 \(70\) 分。
我们考虑优化这个过程。
实际上,我们可以发现,整个过程中存在一个连续变化的过程,当我们不考虑限制条件而让蛇一直吃下去时,一条蛇吃掉另外一条蛇之后,它在之后的轮次中会被其他蛇吃掉,而吃掉它的蛇可能会被另外一条蛇吃掉。设 \(A\) 吃掉了 \(B\),\(C\) 吃掉了 \(A\),\(D\) 吃掉了 \(C\),这个时候,因为 \(C\) 吃了 \(A\) 之后会被吃,所以它不会再去吃 \(A\),\(A\) 就可以放心的吃掉 \(B\)。显然,\(A\) 是否会吃掉 \(B\) 和这个连锁反应的长度有关,这个连锁反应结束的位置在出现一条蛇不会被吃掉。
继续深入思考,我们可以拿到以下结论:
-
当一条蛇吃掉另外一条蛇后仍然是最大值时,它一定会吃。
-
当一条蛇吃掉另外一条蛇后,其不是最小值时,它一定会吃。
结论一非常显然,反正这种情况下之后是否结束游戏的决定权仍然在我,不吃白不吃。结论二虽然不见得容易想到,但它的证明时简单的。即考虑之后的一轮中,最大值一定比上一轮的最大值小,最小值一定比上一轮的最小值大,如果继续吃,一定会得到一个比上一轮更小的值。很显然这个值一定会比上一轮的值更先被吃掉,所以,是否结束游戏时它应该担心的事。
那么,如果我们吃掉另外一条蛇之后,变成最小值了呢?
很显然,这个时候我们就可以使用上述连锁反应的方式来判断当前我们是否应该吃掉这只蛇,而根据我们的结论,我们只需要不停模拟让最大的蛇来吃,直到有一只蛇可以放心吃即吃掉后它的值不是最小值即可。到这里游戏基本也已经结束了,只是看是否能够在多吃掉一只。
这个过程怎么维护呢?
设两个双端队列 \(A\),和 \(B\),满足开端小,结尾大。
每次取出 \(A\) 的队头,再从 \(A\) 和 \(B\) 的队尾选取最大值,如果吃掉后的值最小则开启结束阶段,否则直接将新的值放在 \(B\) 的队头,很显然 \(B\) 会一直满足上述要求,具体证明即结论二。
结束阶段即相当于我们要求出连锁反应的长度,这是简单的。即持续模拟这个过程,直到只剩下两只蛇或者有一只蛇吃掉另外一只蛇后不是最小值即可。得到的新值单独维护,不用再插入队列,因为它本身就是最小值。
时间复杂度 \(O(Tn)\)。
\(code\)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
struct node{
int l, id;
bool operator < (const node &x) const{
if(l == x.l) return id < x.id;
else return l < x.l;
}
}; //蛇的长度、编号
int T, n, k, ans;
int top, a[N], sck[N];
inline void Sol()
{
deque<node> A, B; //头小、尾大
for(register int i = 1; i <= n; i++) A.push_back((node){a[i], i});
while(1){
if(A.size() + B.size() == 2) { ans = 1; break; } //放心吃
node mx, mi = A.front(); A.pop_front();
if(B.empty() || (!A.empty() && B.back() < A.back())) mx = A.back(), A.pop_back(); //A数组中是最大的
else mx = B.back(), B.pop_back();
node u = (node){mx.l - mi.l, mx.id};
if(A.empty() || u < A.front()){ //最小的蛇
ans = A.size() + B.size() + 2; //不吃的情况
int cnt = 0;
while(1){
cnt++;
//只剩下两只蛇了,最后一只蛇肯定可以放心吃,可以直接开始判断
if(A.size() + B.size() + 1 == 2) { ans = ans - ((cnt ^ 1) & 1); break; }
node mx;
if(B.empty() || (!A.empty() && B.back() < A.back())) mx = A.back(), A.pop_back();
else mx = B.back(), B.pop_back();
u = (node){mx.l - u.l, mx.id};
if((A.empty() || u < A.front()) && (B.empty() || u < B.front())) ;
else { ans = ans - ((cnt ^ 1) & 1); break; }
}
break;
}
else B.push_front(u);
}
}
int main()
{
T = read();
bool flag = false;
while(T--){
if(!flag){
n = read(), flag = true;
for(register int i = 1; i <= n; i++) a[i] = read();
}
else{
k = read();
for(register int i = 1, x, y; i <= k; i++) x = read(), y = read(), a[x] = y;
}
Sol();
printf("%d\n", ans);
}
return 0;
}