【DSY 4783】小Z的作业 题解 (可撤销并查集 + 线段树分治)
可撤销并查集 + 线段树分治。
Question
\(n\) 点 \(m\) 条无向边,边由 \(1\) 到 \(m\) 编号,\(q\) 次询问求这 \(n\) 个点以及编号在 \([l,r]\) 区间内的边组成的图 的连通块个数是否 \(\le\) 某个定值 \(k\)。
Solution
0 离线
算是题外话吧,毕竟考场上好不容易想到的。
很显然,就是莫队 + 可撤销并查集。这里有一道弱化离线版,题解附代码实现。
1 在线
回归正题,其实此题的根本思路非常经典的了(见《Tricks 整理》思路部分)。
即,去维护一个 \(f\) 数组,记 \(f_i\) 表示以 \(i\) 为右端点,最小的左端点满足答案为 Yes
。这样一来,通过一个预处理求出 \(f\) 数组,可以将强制在线转化为离线倒序处理(这是感性理解的说法)。
然后考虑求 \(f\) 数组。此题是把一个区间内的所有边添加进去,其余边删掉。
不妨看下图,发现灰色部分所涵盖的边是在统计 \(f_i\) 和 \(f_{i-1}\) 的时候都涉及到的。
(1)线段树分治
也即是说,我们需要一个数据结构,去支持撤销操作,或者说是维护一个操作影响的时间区间。
然后线段树分治(也作时间分治)应运而生。
每个叶子看作一条边,如果第 \(x\) 条边对 \([t_i.l,t_i.r]\) 都有贡献/影响,那么直接在 \(i\) 处把 \(x\) 记录一下,经过时加上,这样在遍历后续儿子们的时候这条边就一直存在。最后 return;
前使用可撤销并查集删掉即可。
具体如何判断第 \(x\) 边对何区间有影响:
- 考虑先求出 \(f_n\),直接从第 \(n\) 条边开始按编号递减顺序加入并判断即可,由于 \(∀i<n,f_i≤f_n\),所以 \(∀i∈[f_n,n]\) ,我们可以直接在 \([i,n]\) 这个区间加入边 \(i\)。
- 接着考虑求出 \(f_{n−1}\),方法类似,从第 \(f_{n−1}\) 条边开始按编号递减顺序加入并判断即可。\(∀i∈[f_{n-1},f_n]\),我们依然可以直接在 \([i,n-1]\) 这个区间加入边 \(i\)。
(2)LCT
其实我们还发现,每次需要删除的边只有第 \(i\) 条而已,动态加边删边,自然就想到是 LCT 了。
使用 LCT,线性倒序扫描,每次判断删掉第 \(i\) 条边之后对连通块个数是否有影响即可。
这个做法写法都很显然了,就不再赘述了。
Code
(1)线段树分治
#include<bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for(register int i = a; i <= b; ++i)
const int maxn = 2e5 + 5;
int n, m, k, tp, q;
int cnt, fa[maxn], sz[maxn];
int st[maxn], top;
struct node{
int u, v;
}e[maxn];
vector <node> s[1 << 20];
int f[maxn]; unsigned lst = 0;
inline int fnd(int x){
return fa[x] == x ? x : fnd(fa[x]);
}
inline void lnk(int x, int y){
x = fnd(x), y = fnd(y);
if(x == y) return;
if(sz[x] > sz[y]) swap(x, y);
st[++top] = x, cnt -= 1, fa[x] = y, sz[y] += sz[x];
}
inline void undo(){
int x = st[top--]; cnt += 1;
sz[fa[x]] -= sz[x], fa[x] = x;
}
inline void updt(int i, int l, int r, int L, int R){
if(L <= l and r <= R){
s[i].push_back(e[L]); return;
} int mid = l + r >> 1;
if(mid >= L) updt(i << 1, l, mid, L, R);
if(mid < R) updt(i << 1 | 1, mid + 1, r, L, R);
}
inline void build(int i, int l, int r){
int init = top; for(auto nw : s[i]) lnk(nw.u, nw.v);
if(l == r){
f[l] = f[l + 1];
while(f[l] > 1){ if(cnt <= k) break;
lnk(e[f[l] - 1].u, e[f[l] - 1].v);
if(cnt <= k) break;
f[l] -= 1; if(f[l] < l) updt(1, 1, m, f[l], l - 1);
} while(top > init) undo(); return;
} int mid = l + r >> 1;
build(i << 1 | 1, mid + 1, r), build(i << 1, l, mid);
while(top > init) undo();
}
int main(){
scanf("%d%d%d%d", &n, &m, &k, &tp);
rep(i, 1, m) scanf("%d%d", &e[i].u, &e[i].v);
rep(i, 1, n) fa[i] = i, sz[i] = 1;
cnt = n, f[m + 1] = m + 1, build(1, 1, m);
scanf("%d", &q);
rep(i, 1, q){ int l, r;
scanf("%d%d", &l, &r);
if(tp) l = (l + lst) % m + 1, r = (r + lst) % m + 1;
if(l > r) swap(l, r); lst <<= 1;
if(f[r] <= l) printf("No\n"); else printf("Yes\n"), lst += 1;
}
return 0;
}
(2)LCT
Code by @kymru
#include <cstdio>
#include <set>
using namespace std;
const int maxn = 2e5 + 5;
const int maxm = 2e5 + 5;
const int nd_sz = 4e5 + 5;
struct link_cut_tree
{
int top, q[nd_sz];
int fa[nd_sz], son[nd_sz][2], idx[nd_sz], val[nd_sz];
bool rev[nd_sz];
void push_up(int x) { val[x] = max(max(val[son[x][0]], val[son[x][1]]), idx[x]); }
void push_down(int x)
{
if (rev[x])
{
rev[son[x][0]] ^= 1;
rev[son[x][1]] ^= 1;
rev[x] = false;
swap(son[x][0], son[x][1]);
}
}
bool get(int x) { return (son[fa[x]][1] == x); }
bool is_root(int x) { return (son[fa[x]][0] != x) && (son[fa[x]][1] != x); }
void rotate(int x)
{
int y = fa[x], z = fa[y], k = get(x);
son[y][k] = son[x][k ^ 1];
fa[son[x][k ^ 1]] = y;
son[x][k ^ 1] = y;
if (!is_root(y)) son[z][son[z][1] == y] = x;
fa[x] = z, fa[y] = x;
push_up(y), push_up(x);
}
void splay(int x)
{
q[top = 1] = x;
for (int i = x; !is_root(i); i = fa[i]) q[++top] = fa[i];
for (int i = top; i; i--) push_down(q[i]);
while (!is_root(x))
{
int y = fa[x], z = fa[y];
if (!is_root(y)) rotate(get(x) == get(y) ? y : x);
rotate(x);
}
}
void access(int x)
{
for (int t = 0; x; t = x, x = fa[x])
{
splay(x);
son[x][1] = t;
push_up(x);
}
}
void make_root(int x)
{
access(x);
splay(x);
rev[x] ^= 1;
}
int find(int x)
{
access(x);
splay(x);
while (son[x][0]) x = son[x][0];
return x;
}
void split(int x, int y)
{
make_root(x);
access(y);
splay(y);
}
void cut(int x, int y)
{
split(x, y);
if ((son[y][0] == x) && (son[x][1] == 0)) son[y][0] = fa[x] = 0;
}
void link(int x, int y)
{
make_root(x);
fa[x] = y;
}
} tree;
int n, m, k, tp, q;
int ui[maxm], vi[maxm], rgt[maxm];
set<int> idx;
int main()
{
unsigned last_ans = 0;
scanf("%d%d%d%d", &n, &m, &k, &tp);
k = n - k;
for (int i = 1; i <= m; i++) scanf("%d%d", &ui[i], &vi[i]);
for (int i = 1; i <= n + m; i++) tree.idx[i] = tree.val[i] = i;
for (int i = m; i >= 1; i--)
{
if (ui[i] != vi[i])
{
if (tree.find(ui[i]) == tree.find(vi[i]))
{
tree.split(ui[i], vi[i]);
int pos = tree.val[vi[i]];
tree.cut(ui[pos - n], pos);
tree.cut(pos, vi[pos - n]);
idx.erase(pos - n);
}
tree.link(ui[i], n + i);
tree.link(n + i, vi[i]);
idx.insert(i);
}
if (idx.size() > k)
{
int pos = (*(--idx.end()));
idx.erase(pos);
tree.cut(ui[pos], n + pos);
tree.cut(n + pos, vi[pos]);
}
if (idx.size()) rgt[i] = (idx.size() == k ? (*(--idx.end())) : m + 1);
else rgt[i] = (k ? m + 1 : 0);
}
scanf("%d", &q);
while (q--)
{
int l, r;
scanf("%d%d", &l, &r);
if (tp == 1)
{
l = (l + last_ans) % m + 1;
r = (r + last_ans) % m + 1;
if (l > r) swap(l, r);
}
puts(r >= rgt[l] ? "Yes" : "No");
last_ans <<= 1;
if (r >= rgt[l]) last_ans++;
}
return 0;
}
感谢阅读。