带撤销并查集 & 可持久化并查集

带撤销并查集支持从某个元素从原来的集合中撤出来,然后加入到一个另外一个集合中,或者删除该元素

用一个映射来表示元素和并查集中序号的关系,代码中用\(to[x]\) 表示x号元素在并查集中的 id

删除 x 号元素时,需要将 \(to[x]\) 的集合大小减去1,然后令 \(to[x]=-1\) 标记 x 删除即可

如果要重新加入一个元素,那么给x分配一个新的 id,\(to[x] = newid\)

例题1:https://www.cometoj.com/contest/33/problem/E?problem_id=1459

#include <bits/stdc++.h>
using namespace std;
const int N = 4000010;
int fa[N],to[N],sz[N],cnt,a[N],b[N],tot;
int n,m,k,x,y;
int ok[N];
int find(int x){
    return x == fa[x]?x:fa[x] = find(fa[x]);
}
void merge(int x,int y){
    int a = find(to[x]);
    int b = find(to[y]);
    if(a==b) return;
    fa[b] = a;
    sz[a] += sz[b]; sz[b] = 0;
}
// 将x从原集合删除,加入到 y 所属的集合
void update(int x,int y){
    sz[find(to[x])]--;
    to[x] = ++cnt;//给x分配新的 id
    sz[cnt] = 1;
    fa[cnt] = cnt;
    merge(x,y);
}
int main(){
    scanf("%d%d",&n,&m);cnt = n;
    for(int i=1;i<=n;i++)fa[i] = i,to[i] = i,sz[i] = 1;
    for(int i=1;i<=m;i++){
        scanf("%d%d",&k,&x);if(k!=3)scanf("%d",&y);
        if(k == 5){
            a[++tot] = x;
            b[tot] = y;continue;
        }
        if(k == 1)merge(x,y);
        if(k == 2)update(x,y);
        if(k == 4){
            if(find(to[x]) == find(to[y]))printf("Yes\n");
            else printf("No\n");
        }

        if(k == 3)printf("%d\n",sz[find(to[x])]-1);
    }
    
    for(int i=1;i<=tot;i++){
        if(find(to[a[i]]) == find(to[b[i]])) ok[find(to[a[i]])] = 1;
    }
    int res = -1;
    for(int i=1;i<=n;i++)
        if(!ok[find(to[i])])res = max(res,sz[find(to[i])]);
    cout<<res<<endl;
    return 0;
}

例题2:https://nanti.jisuanke.com/t/42576
[2019南昌区域赛A]
不要求在线的可持久化操作,可以离线处理询问,按照正常的时间顺序维护并查集,由于需要回溯,所以不能使用路径压缩
复杂度 \(O(m\log n)\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
#define dbg(x...) do { cout << "\033[32;1m" << #x <<" -> "; err(x); } while (0)
void err() { cout << "\033[39;0m" << endl; }
template<class T, class... Ts> void err(const T& arg,const Ts&... args) { cout << arg << " "; err(args...); }
const int N = 1000000 + 50;
struct Node{
    int op, x, y;
}o[N];
vector<int> g[N];
int n, m;
int fa[N*2], dep[N*2], sz[N*2], to[N], tot;
int res[N];
int find(int x){
    if(x == fa[x]) return x;
    return find(fa[x]);
}
/*
1 k a b merge(a, b)
2 k a  del(a)
3 k a b merge(a,b)
4 k a b find(a) == find(b)
5 k a  sz[a]
*/
void dfs(int u){
    for(auto id : g[u]){
        int op = o[id].op, x = o[id].x, y = o[id].y;
        if(op == 1){
            if(to[x] == -1 || to[y] == -1){
                dfs(id);
                continue;
            }
            int rx = find(to[x]), ry = find(to[y]);
            if(rx == ry){ dfs(id); continue;}
            else if(dep[rx] == dep[ry]){
                fa[ry] = rx; sz[rx] += sz[ry]; dep[rx] ++;
                dfs(id);
                fa[ry] = ry; sz[rx] -= sz[ry]; dep[rx] --;
            } else {
                if(dep[rx] < dep[ry]) swap(rx, ry);
                fa[ry] = rx; sz[rx] += sz[ry];
                dfs(id);
                fa[ry] = ry; sz[rx] -= sz[ry];
            }
        } else if(op == 2){
            if(to[x] == -1) { dfs(id); continue; }
            int rx = find(to[x]);
            int t = to[x];
            to[x] = -1;
            sz[rx] --;
            dfs(id);
            to[x] = t;
            sz[rx] ++;
        } else if(op == 3){
            if(to[x] == - 1 || to[y] == -1) {dfs(id); continue; }
            int t = to[x];
            int rx = find(to[x]), ry = find(to[y]);
            sz[rx]--;
            to[x] = ++tot;
            sz[to[x]] = 1;
            fa[to[x]] = ry;
            sz[ry] ++;
            dfs(id);
            sz[ry] --;
            sz[rx] ++;
            to[x] = t;
        } else {
            if(op == 4){
                if(to[x] == -1 || to[y] == -1){
                    res[id] = 0;
                } else {
                    res[id] = find(to[x]) == find(to[y]);
                }
            } else if(op == 5){
                if(to[x] == -1) res[id] = 0;
                else {
                    res[id] = sz[find(to[x])];
                }
            }
            dfs(id);
        }
    }
}
int main(){
    scanf("%d%d", &n, &m);
    for(int i=1;i<=m;i++){
        int op, k, x, y = 0;scanf("%d%d%d", &op, &k, &x);
        g[k].push_back(i);
        if(op != 2 && op != 5) scanf("%d", &y);
        o[i] = {op, x, y};
    }
    for(int i=1;i<=n;i++) to[i] = i, fa[i] = i, sz[i] = 1;
    tot = n;
    dfs(0);
    for(int i=1;i<=m;i++){
        if(o[i].op == 4) puts(res[i] ? "Yes" : "No");
        else if(o[i].op == 5){
            printf("%d\n", res[i]);
        }
    }
    return 0;
}

可持久化并查集支持查询任一历史版本的信息。并查集信息用数组 \(fa\) 表示,合并集合时,基本操作有两种,一个是路径压缩(可能会修改很多个fa),一个是按秩合并(启发式合并思路,小的集合向大的合并,只会修改一次fa),由于要保存历史信息,按照可持久化的一贯思路,每一次操作都会新开 logn 的空间,所以这里要用按秩合并。

注意按秩合并的find函数写法也有所不同,不能修改fa

此时问题就是维护每个版本的fa数组

模版题:https://www.luogu.com.cn/problem/P3402

const int N = 200000 + 5;
int n, m;
struct TreeNode{
    int l, r;
}t[N*30];
int root[N], tot;
int fa[N*30], dep[N*30];
void build(int &p, int l, int r){
    p = ++tot;
    if(l == r){fa[p] = l;return;}
    int mid = l + r >> 1;
    build(t[p].l, l, mid);
    build(t[p].r, mid+1, r);
}
// 修改p版本的pos位置的fa值
void change(int last, int &p, int l, int r, int pos, int val){
    p = ++tot;
    t[p] = t[last];
    if(l == r){
        fa[p] = val;
        dep[p] = dep[last];
        return;
    }
    int mid = l + r >> 1;
    if(pos <= mid)
        change(t[last].l, t[p].l, l, mid, pos, val);
    else 
        change(t[last].r, t[p].r, mid+1, r, pos, val);
} 
int getIndex(int p, int l, int r,int pos){
    if(l == r) return p;
    int mid = l + r >> 1;
    if(pos <= mid) return getIndex(t[p].l, l, mid, pos);
    else return getIndex(t[p].r, mid+1, r, pos);
}
int find(int p, int x){
    int index = getIndex(p, 1, n, x);
    if(fa[index] == x) return index; // 返回祖先节点下标
    return find(p, fa[index]);
}
void merge(int last, int &p, int x, int y){
    int fx = find(p, x), fy = find(p, y);
    if(fa[fx] == fa[fy]) return;
    // x-> y合并,需要满足 dep[x]<dep[y]
    if(dep[fx] > dep[fy]) swap(fx, fy);
    change(last, p, 1, n, fa[fx], fa[fy]);
    if(dep[fx] == dep[fy]){
        dep[getIndex(p, 1, n, fa[fy])]++;
    }
}
int main(){
    scanf("%d%d", &n, &m);
    build(root[0], 1, n);
    for(int i=1;i<=m;i++){
        root[i] = root[i-1];
        int op, x, y;scanf("%d%d", &op, &x);
        if(op == 1){
            scanf("%d", &y);
            merge(root[i], root[i], x, y);
        } 
        else if(op == 2) root[i] = root[x];
        else {
            scanf("%d", &y);
            int fx = find(root[i], x), fy = find(root[i], y);
            if(fa[fx] == fa[fy]) puts("1");
            else puts("0");
        }
    }
    return 0;
}

例题:https://www.luogu.com.cn/problem/P4768

先跑一次最短路,然后按照边权从大到小用带权并查集维护集合到源点的最短距离。保留所有版本的信息,然后对于每次查询找到对应版本号回答问题即可

复杂度:\(O(n\log m + (m+q) \log^2n)\)

posted @ 2020-04-28 17:20  kpole  阅读(1269)  评论(0编辑  收藏  举报