CF1956F Nene and the Passing Game 题解
处理很妙的题,部分细节请教了 未来姚班zyl 和 LYH_cpp,在此鸣谢。
首先考虑把题目给的式子进行转化,设 \(i < j\),那么 \(i\) 和 \(j\) 能传球当且仅当 \(l_i + l_j \le j - i \le r_i + r_j\)。
移项并拆开得到,\(i + l_i \le j - l_i\) 且 \(i + r_i \ge j - r_j\),如果画到数轴上的话就能发现这个条件等价于 \(i\) 的右半区间和 \(j\) 的左半区间相交了。
如果把这种能传球的关系看作是一条边,那么不难发现,一个连通块内的所有点都能在一次计划内完成,那么答案就是连通块的个数。
于是我们有了一个 \(O(n^2\alpha(n))\) 的做法,枚举两两点然后连边查答案。
考虑怎么优化,这里用到了处理连通性问题时常用的方法。
我们转换思路,从枚举人连边变成枚举人然后向它的左右区间连边,通过值域上的虚点保证连通性。
注意到这是一个经典的点 \(x\) 向区间 \([l, r]\) 连边查连通块个数的做法,可行的做法有 \(O(n \log n\alpha(n))\) 的线段树优化建图,以及均摊 \(O(n\alpha(n))\) 的并查集做法。
不过我们忽视了一个问题,如果通过中间点构造联通的话,会出现多个点都通过左区间或者都通过右区间相连的情况。
对于这种情况,我们有这样精妙的处理:
我们考虑只保留同时被某个点的左区间覆盖并且同时被某个点的右区间覆盖的点。这样为什么是对的呢?我们分情况来考虑。
如果一个点只被左区间或者只被右区间覆盖到了,显然这个时候没有点能够通过该点进行
左-右
的联通。而如果一个点两种区间都有,那么由于题目提到了 一个人可以在一次训练中重复多次,那么假设覆盖的左区间分别来自 \(L_1, L_2, \cdots, L_n\),右区间至少有一个,设其为 \(R\),有方案 \(L_1RL_2R\cdots L_nR\) 可以是一种合法的方案,并且完成了所有覆盖该点的任务。
于是我们可以用差分维护值域上的每个点是否同时被左右区间覆盖了,时间复杂度 \(O(n\alpha(n))\)。
// 如果命运对你缄默, 那就活给他看。
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast", "inline", "-ffast-math")
#pragma GCC target("avx,sse2,sse3,sse4,mmx")
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
// #define int LL
const int maxn = 4e6 + 10;
int dl[maxn], dr[maxn], n;
int l[maxn], r[maxn];
int f[maxn];
int pr[maxn], nx[maxn], tot;
int v[maxn];
inline int find(int x) { return f[x] == x ? f[x] : f[x] = find(f[x]); }
inline void clear() {
tot = 0;
fill(v + 1, v + 1 + n, 0);
fill(dl + 1, dl + 1 + n, 0);
fill(dr + 1, dr + 1 + n, 0);
fill(f + 1, f + n + n + 1, 0);
fill(pr + 1, pr + 1 + n, 0);
fill(nx + 1, nx + 1 + n, 0);
}
inline void mg(int x, int y) {
x = find(x), y = find(y);
if(x ^ y) f[y] = x;
}
inline void solve() {
cin >> n;
clear();
for(int i = 1; i <= n; ++ i) {
f[i] = i;
cin >> l[i] >> r[i];
dl[max(1, i - r[i])] ++ ;
dl[max(1, i - l[i] + 1)] -- ;
dr[min(n, i + l[i])] ++ ;
dr[min(n, i + r[i] + 1)] -- ;
}
for(int i = 1; i <= n; ++ i) {
dl[i] += dl[i - 1];
dr[i] += dr[i - 1];
if(dl[i] && dr[i]) {
pr[i] = nx[i] = ++ tot;
f[n + tot] = n + tot;
} else {
pr[i] = pr[i - 1];
}
}
nx[n + 1] = INT_MAX;
for(int i = n; i; -- i) {
if(!nx[i]) {
nx[i] = nx[i + 1];
}
}
for(int i = 1, ql, qr; i <= n; ++ i) {
ql = nx[max(1, i - r[i])];
qr = pr[max(0, i - l[i])];
if(ql <= qr) {
v[ql] ++, v[qr] -- ;
mg(i, qr + n);
}
ql = nx[min(n + 1, i + l[i])];
qr = pr[min(n, i + r[i])];
if(ql <= qr) {
v[ql] ++, v[qr] -- ;
mg(i, qr + n);
}
}
for(int i = 1; i <= tot; ++ i) v[i] += v[i - 1];
for(int i = 1; i <= tot; ++ i) {
if(v[i]) {
mg(i + n, i + 1 + n);
}
}
int ans = 0;
for(int i = 1; i <= n + tot; ++ i) {
ans += f[i] == i;
}
cout << ans << '\n';
}
signed main() {
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T;
cin >> T;
while(T -- ) solve();
return 0;
}