可撤销并查集
定义及用处
由于众所周知,并查集只可以加边而不可以删边,所以有了可撤销并查集。
可撤销并查集只可以按照加入的时间从后到前撤销加边操作。
常用于一些连通性问题,并且很多时候会调整处理的顺序,只要按照上述撤销顺序撤销边即可。
思路与实现
可撤销在有时可以通过改变加入顺序中途撤销求解问题。那又怎么做呢?路径压缩会改变树的形态,所以只用按秩合并(按子树大小合并,小的那颗合并到大的上面去),单次时间复杂度为
具体的,我们会把所有加入的边压入一个栈,然后当什么时候要撤销时不断从栈顶弹出一条边,撤销掉。而至于具体的撤销步骤,我们假设此边原来是把
那么对于一条边为什么一定要是有顺序的撤销呢?如果不是按出栈的顺序撤销,那么必定有比他晚一些连边的集合的大小没法维护,所以必须按出栈顺序撤销。
模板:
struct DSU{
int n = 0, tot = 0, fa[N], sz[N], s[N];
void ins(){n++, fa[n] = n, sz[n] = 1;}//插入节点
int F(int x){return fa[x] == x? x : F(fa[x]);}//即find查找函数
void U(int x, int y){//合并函数
x = F(x), y = F(y);
if(x == y) return;
if(sz[x] < sz[y]) swap(x, y);
s[++tot] = y, fa[y] = x, sz[x] += sz[y];
}
void D(){//删除栈顶边
if(!tot) return;
int y = s[tot--]; sz[fa[y]] -= sz[y], fa[y] = y;
}
void back(int t = 0){while(tot > t) D();}//删除到只剩t条边
}d;
例题:可持久化并查集
我们用可撤销并查集离线玩。直接做很难,因为撤销操作可以撤销之前的撤销操作,这样很可能时嵌套的,所以我们考虑建成一棵树:如果是第
void Dfs(int u){
t[u] = d.tot;
if(typ[u] == 1) d.U(a[u], b[u]);//合并操作
if(typ[u] == 3) ans[u] = (d.F(a[u]) == d.F(b[u]));//询问
E(i, u) Dfs(e[i].v); d.back(t[u]);
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof(h));
L(i, 1, n) d.ins();
L(i, 1, m){
int opt; scanf("%d", &opt), typ[i] = opt;
if(opt == 1 || opt == 3)
scanf("%d%d", &a[i], &b[i]), Adde(i - 1, i);
else//回到历史版本
scanf("%d", &a[i]), Adde(a[i], i);
}
Dfs(0);
L(i, 1, m) if(typ[i] == 3) printf("%d\n", ans[i]);
return 0;
}
例题:CF891C
这道题与很多可撤销并查集的题目一样,都是需要通过一系列操作转换成可以使用可撤销并查集。可撤销并查集和其它数据结构一样,只是作为实现过程中的一种工具。
这道题我们离线做。首先,我们知道最小生成树涉及连通性,就是如果第
bool cmp(E x, E y){return x.w < y.w;}//w从小到大排序
bool cmp2(Q x, Q y){return (g[x.x].w == g[y.x].w)? (x.i < y.i) : (g[x.x].w < g[y.x].w);}//按边的权值w排序,w相同按照询问的编号排序,这样可以使边权相同且是同个询问的排在一起。
int main(){
scanf("%d%d", &n, &m);
L(i, 1, n) d.ins();
L(i, 1, m)
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w), g[i] = e[i];
sort(e + 1, e + m + 1, cmp);
scanf("%d", &q);
L(i, 1, q){
int k, x; scanf("%d", &k), ans[i] = 1;
L(j, 1, k)
scanf("%d", &x), t[++tot] = {i, x};//每个询问的每条边加入进去
}
sort(t + 1, t + tot + 1, cmp2);
int id = 1;
L(i, 1, tot){
if(!ans[t[i].i]) continue;
if(i > 1 && (g[t[i - 1].x].w < g[t[i].x].w || t[i - 1].i < t[i].i))//询问不同或w不同
d.back();
while(id <= m && e[id].w < g[t[i].x].w)//把小于当前边的w的全部加进去
d.U(e[id].u, e[id].v, 0), id++;//第三个参数0表示不放到撤销列表中
if(d.F(g[t[i].x].u) == d.F(g[t[i].x].v))//不合法
ans[t[i].i] = 0;//更新答案
else
d.U(g[t[i].x].u, g[t[i].x].v, 1);//合并
}
L(i, 1, q)
puts(ans[i]? "YES" : "NO");
return 0;
}
可撤销并查集与分治
可撤销并查集常数巨小,常和分治一起使用,只可惜是带了两只
套路?
- 将边加入并查集中。
- 递归解决问题。
- 在递归的最后一层处理询问。
- 撤销边,并返回。
框架:
void DNC(int l, int r){
if(l == r){
//update ans
return;
}
int mid = l + r >> 1, tm = d.tot;//tm为加边前的状态,回退用
L(i, mid + 1, r)
//add edge
DNC(l, mid), d.back(tm);//回到历史状态
L(i, l, mid)
//add edge
DNC(mid + 1, r), d.back(tm);//回到历史状态
}
例题:CF1681F
这题是可撤销并查集与分治结合的模板题。
首先,我们能得到一个结论,对于一条边而言,把图上所有权值等于该边的边全部删除,那么新图中,这条边的两个端点的联通块大小的乘积即对这条边对答案的贡献。
我们不妨把权值看作节点,而权值最多有不超过
时间分治/线段树分治
即我们每条边有个存在的时间,然后要求对每个时间进行查询一些并查集可做的操作,比如是否联通啊,或者什么判定二分图啊之类的。
暴力的不用说,想必大家都会,对每个时间开个并查集,然后暴力加边,暴力查询。
我们把暴力优化一下,我们采用线段树——我们把边加到线段树的节点上。即我们对线段树的每个节点开个动态数组,然后像普通线段树的修改那样把区间的动态数组里加入这条边(不需要懒标记),然后我们离线查询,遍历一遍整颗线段树处理出所有答案。进入一个节点时,我们加入属于这整个区间的边(当前节点的动态数组里的边);出去时,删除之前加入的边。那么我们对每个时间节点维护的是根到其路径上所有的边构成的并查集,也就是这个时间节点里存在的所有边。
模板(这是处理询问的模板):
void Query(int p, int l, int r) {
//加边
if (l == r) {
//处理询问
return;
}
int mid = l + r >> 1;
Query(p << 1, l, mid), Query(p << 1 | 1, mid + 1, r);
//删边
}
我们思考时间复杂度是什么,即建树以及最后的遍历线段树是
所以最高的时间复杂度是边数乘一个并查集的复杂度。
例题:二分图 /【模板】线段树分治
最模板的题,按照上述模板做即可,代码贴一段。
void Update(int p, int l, int r, int x, int y, pair<int, int> pr){
if(x <= l && r <= y){
t[p].push_back(pr); return;
}
int mid = l + r >> 1;
if(x <= mid) Update(p << 1, l, mid, x, y, pr);
if(mid < y) Update(p << 1 | 1, mid + 1, r, x, y, pr);
}
void Query(int p, int l, int r){
int ti = d.top;
for(auto pr: t[p]){
int x = pr.first, y = pr.second;
if(d.F(x) == d.F(y)){d.back(ti); return;}
d.U(x, y + n), d.U(x + n, y);
}
if(l == r){ans[l] = 1, d.back(ti); return;}
int mid = l + r >> 1;
Query(p << 1, l, mid), Query(p << 1 | 1, mid + 1, r);
d.back(ti);
}
例题:[SDOI2008] 洞穴勘测
首先,我们通过排序和处理能得到每条边存在的时间(这里的时间是对操作而言的),然后进行上述模板题。询问的话利用动态数组存每个时间点的询问。还是比较模板化。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现