AIGC标识 可持久化并查集详解

C++ 可持久化并查集详解

说明

本文系统讲解可持久化并查集(Persistent DSU),覆盖原理、实现、用法、变形、定理、推论、典型例题与高级扩展。

总目录

  1. 可持久化数据结构基础
  2. 并查集基础回顾
  3. 可持久化并查集原理
  4. 可持久化数组(主席树)实现
  5. 可持久化并查集实现
  6. 复杂度分析与优化
  7. 常见变形与用法
  8. 典型例题精讲(含你所给两道题)
  9. 高级话题与研究扩展
  10. 相关定理、推论、公式速查
  11. 代码优化技巧
  12. 与其他可持久化结构结合
  13. 常见错误与调试
  14. 完整模板代码

1. 可持久化数据结构基础

1.1 什么是可持久化

可持久化(Persistent):数据结构在修改时保留历史版本,允许访问任意历史版本。
两种类型:

  • 部分可持久化(Partially Persistent):所有版本可读,只有最新版本可写。
  • 完全可持久化(Fully Persistent):所有版本可读可写。

并查集通常使用部分可持久化

1.2 路径复制(Path Copying)

修改一个节点时,复制从根到该节点的路径上所有节点,形成新版本。
未修改的子树共享旧版本,节省空间。
复杂度:单次修改 \(O(\log n)\) 新节点。

        v0                     v1
       /  \                   /  \
      A    B     修改A        A'   B
     / \  / \    ----->      / \
    ...  ...                ...

1.3 路径压缩 vs 路径复制

  • 路径压缩会修改大量节点,导致可持久化空间爆炸。
  • 可持久化并查集不能用路径压缩,只能用按秩合并
  • 按秩合并保证树高 \(O(\log n)\)

1.4 可持久化的三种方式

方法 空间 时间 适用
路径复制 \(O(\log n)\)/次 \(O(\log n)\) 树形结构
Fat Node 摊还 \(O(1)\) \(O(\log m)\) 一般
Node Copying \(O(1)\) 摊还 \(O(1)\) 完全可持久化

2. 并查集基础回顾

2.1 定义

并查集(Disjoint Set Union, DSU)维护不相交集合,支持:

  • find(x):查 \(x\) 所在集合代表元。
  • union(x,y):合并两集合。

2.2 优化

  • 路径压缩f[x]=find(f[x]),摊还 \(O(\alpha(n))\)
  • 按秩合并:小树连大树,树高 \(O(\log n)\)
  • 同时使用\(O(\alpha(n))\)

2.3 反阿克曼函数

\[\alpha(n)=\min\{k:A(k,k)\ge n\} \]

其中 \(A\) 是阿克曼函数。
对于所有实际 \(n\)\(\alpha(n)\le4\)

2.4 并查集复杂度

优化 单次摊还
\(O(n)\)
仅路径压缩 \(O(\log n)\)
仅按秩合并 \(O(\log n)\)
两者 \(O(\alpha(n))\)

2.5 定理(Tarjan 1975)

仅用路径压缩的并查集,单次操作摊还 \(O(\log n)\),序列操作 \(O(n\log n)\)
同时使用路径压缩与按秩合并,摊还 \(O(\alpha(n))\)


3. 可持久化并查集原理

3.1 核心思想

  • f 数组(父指针)与 rank/dep 数组存入可持久化数组
  • 每次操作基于上一版本,产生新版本。
  • find 不使用路径压缩(会破坏历史版本),仅按秩合并。
  • 树高 \(O(\log n)\)find 复杂度 \(O(\log n)\)

3.2 数据结构选择

可持久化数组存储 fa[i]dep[i]
常见实现:可持久化线段树(主席树)

3.3 为什么不用路径压缩

路径压缩会一次性修改从 \(x\) 到根的路径上所有节点。
在可持久化结构上,每次操作需复制整条路径,空间 \(O(\log n)\)\(O(\log^2 n)\) 甚至更高。
而且路径压缩是摊还的,与可持久化的版本隔离语义冲突。

3.4 按秩合并保证树高

引理:按秩合并的并查集,树高 \(\le \lfloor\log_2 n\rfloor+1\)
证明:秩为 \(r\) 的树至少有 \(2^r\) 个节点,由归纳可得。

推论find 复杂度 \(O(\log n)\)

3.5 可持久化并查集复杂度

操作 时间 空间
建树 \(O(n)\) \(O(n)\)
union \(O(\log^2 n)\) \(O(\log n)\)
find \(O(\log^2 n)\) \(O(1)\)
回退 \(O(1)\) \(O(1)\)

其中 \(O(\log^2 n)\) 来自:主席树查询 \(O(\log n)\),树高 \(O(\log n)\)

3.6 图示

版本0:  [1][2][3][4][5]      每个点自成一集合
版本1:  union(1,2)  ->  fa[2]=1
版本2:  union(3,4)  ->  fa[4]=3
版本3:  union(1,3)  ->  fa[3]=1

每个版本对应一个可持久化线段树的根 root[i]


4. 可持久化数组(主席树)

4.1 定义

可持久化数组支持:

  • 单点修改,保留历史版本。
  • 单点查询任意历史版本。

4.2 实现

以值域 \([1,n]\) 建线段树,每个叶节点存一个数组元素。
修改时复制路径。

4.3 建树

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m,cnt;
int root[N],lc[N*40],rc[N*40],val[N*40];
int a[N];
void build(int &k,int l,int r)
{
    k=++cnt;
    if(l==r)
    {
        val[k]=a[l];
        return;
    }
    int mid=l+r>>1;
    build(lc[k],l,mid);
    build(rc[k],mid+1,r);
}
void change(int pre,int &now,int l,int r,int pos,int v)
{
    now=++cnt;
    lc[now]=lc[pre];
    rc[now]=rc[pre];
    if(l==r)
    {
        val[now]=v;
        return;
    }
    int mid=l+r>>1;
    if(pos<=mid)change(lc[pre],lc[now],l,mid,pos,v);
    else change(rc[pre],rc[now],mid+1,r,pos,v);
}
int query(int k,int l,int r,int pos)
{
    if(l==r)return val[k];
    int mid=l+r>>1;
    if(pos<=mid)return query(lc[k],l,mid,pos);
    else return query(rc[k],mid+1,r,pos);
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    build(root[0],1,n);
    for(int i=1;i<=m;i++)
    {
        int v,op,pos,x;
        cin>>v>>op>>pos;
        if(op==1)
        {
            cin>>x;
            change(root[v],root[i],1,n,pos,x);
        }
        else
        {
            root[i]=root[v];
            cout<<query(root[i],1,n,pos)<<"\n";
        }
    }
    return 0;
}

4.4 复杂度

  • 建树:时间 \(O(n)\),空间 \(O(n)\)
  • 修改:时间 \(O(\log n)\),空间 \(O(\log n)\)
  • 查询:时间 \(O(\log n)\),空间 \(O(1)\)

4.5 主席树空间估算

每次修改新建 \(\lceil\log_2 n\rceil+1\) 个节点。
总节点数 \(\le n+4\cdot m\cdot\log_2 n\)
对于 \(n,m=2\cdot10^5\):约 \(2\cdot10^5+4\cdot2\cdot10^5\cdot18\approx1.46\cdot10^7\)
每个节点 3 个 int(64 位下 8 字节),约 350MB。
实际使用中数组开 40*N 左右。


5. 可持久化并查集实现

5.1 存储设计

  • fa[]dep[] 各用一棵可持久化线段树。
  • 但更常见:两棵合为一棵,或者共用根但分两次修改。
  • 推荐fadep 分别存储,但共用版本根 root[i]

实际有两种方案:

方案 A:两棵树,共用版本号

root_fa[i]root_dep[i]
每次版本 i 对应两个根。

方案 B:一棵树,节点存 (fa, dep)

结构紧凑,但修改时 fadep 可能同时更新,需一次修改双值。

常用方案 B,或者把 fadep 放在同一个主席树节点。

5.2 方案 A:双树共用版本

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m,cnt;
int rootfa[N],rootdep[N];
int lc[N*40],rc[N*40],fa[N*40],dep[N*40];
void build(int &k,int l,int r)
{
    k=++cnt;
    if(l==r)
    {
        fa[k]=l;
        dep[k]=1;
        return;
    }
    int mid=l+r>>1;
    build(lc[k],l,mid);
    build(rc[k],mid+1,r);
}
void change(int pre,int &now,int l,int r,int pos,int v,int *arr)
{
    now=++cnt;
    lc[now]=lc[pre];
    rc[now]=rc[pre];
    if(l==r)
    {
        arr[now]=v;
        return;
    }
    int mid=l+r>>1;
    if(pos<=mid)change(lc[pre],lc[now],l,mid,pos,v,arr);
    else change(rc[pre],rc[now],mid+1,r,pos,v,arr);
}
int query(int k,int l,int r,int pos,int *arr)
{
    if(l==r)return arr[k];
    int mid=l+r>>1;
    if(pos<=mid)return query(lc[k],l,mid,pos,arr);
    else return query(rc[k],mid+1,r,pos,arr);
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    build(rootfa[0],1,n);
    // rootdep[0] 与 rootfa[0] 共享结构
    rootdep[0]=rootfa[0];
    for(int i=1;i<=m;i++)
    {
        int op;
        cin>>op;
        if(op==1)
        {
            int a,b;
            cin>>a>>b;
            rootfa[i]=rootfa[i-1];
            rootdep[i]=rootdep[i-1];
            // find + union
        }
        else if(op==2)
        {
            int k;
            cin>>k;
            rootfa[i]=rootfa[k];
            rootdep[i]=rootdep[k];
        }
        else
        {
            int a,b;
            cin>>a>>b;
            rootfa[i]=rootfa[i-1];
            rootdep[i]=rootdep[i-1];
            // query
        }
    }
    return 0;
}

注意:方案 A 中 fadep 在两棵树中,修改时两棵树都要复制路径,节点数翻倍。
一般直接用方案 B:单树双值。

5.3 方案 B:单树双值(推荐)

节点存 fadep
修改时可能只改 fa,或只改 dep,或同时改。
需要在 change 中复制整个路径。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m,cnt;
int root[N];
int lc[N*40],rc[N*40],fa[N*40],dep[N*40];
void build(int &k,int l,int r)
{
    k=++cnt;
    if(l==r)
    {
        fa[k]=l;
        dep[k]=1;
        return;
    }
    int mid=l+r>>1;
    build(lc[k],l,mid);
    build(rc[k],mid+1,r);
}
void change(int pre,int &now,int l,int r,int pos,int f,int d)
{
    now=++cnt;
    lc[now]=lc[pre];
    rc[now]=rc[pre];
    if(l==r)
    {
        fa[now]=f;
        dep[now]=d;
        return;
    }
    int mid=l+r>>1;
    if(pos<=mid)change(lc[pre],lc[now],l,mid,pos,f,d);
    else change(rc[pre],rc[now],mid+1,r,pos,f,d);
}
pair<int,int>query(int k,int l,int r,int pos)
{
    if(l==r)return {fa[k],dep[k]};
    int mid=l+r>>1;
    if(pos<=mid)return query(lc[k],l,mid,pos);
    else return query(rc[k],mid+1,r,pos);
}
int find(int x,int i)
{
    while(1)
    {
        auto p=query(root[i],1,n,x);
        if(p.fi==x)return x;
        x=p.fi;
    }
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    build(root[0],1,n);
    for(int i=1;i<=m;i++)
    {
        int op;
        cin>>op;
        if(op==1)
        {
            int a,b;
            cin>>a>>b;
            root[i]=root[i-1];
            int x=find(a,i);
            int y=find(b,i);
            if(x==y)continue;
            auto px=query(root[i],1,n,x);
            auto py=query(root[i],1,n,y);
            if(px.se<py.se)swap(x,y),swap(px,py);
            // 修改 y 的 fa 为 x
            int tmp;
            change(root[i],tmp,1,n,y,x,px.se);
            root[i]=tmp;
            if(px.se==py.se)
            {
                // 修改 x 的 dep+1
                change(root[i],tmp,1,n,x,x,px.se+1);
                root[i]=tmp;
            }
        }
        else if(op==2)
        {
            int k;
            cin>>k;
            root[i]=root[k];
        }
        else
        {
            int a,b;
            cin>>a>>b;
            root[i]=root[i-1];
            int x=find(a,i);
            int y=find(b,i);
            cout<<(x==y?1:0)<<"\n";
        }
    }
    return 0;
}

6. 复杂度分析与优化

6.1 时间复杂度

  • 建树:\(O(n)\)
  • find:树高 \(O(\log n)\),每层主席树查询 \(O(\log n)\),总 \(O(\log^2 n)\)
  • union:两次 find + 两次修改,\(O(\log^2 n)\)
  • 回退:\(O(1)\)

6.2 空间复杂度

  • 初始:\(O(n)\)
  • 每次 union:最多两次 change,每次 \(O(\log n)\),总 \(O(\log n)\)
  • \(m\) 次操作:\(O(n+m\log n)\)

6.3 优化方向

  1. 单次 change 修改 fadep:若能一次修改两值,减少一半节点。
  2. 迭代代替递归:减少函数调用开销。
  3. 压缩节点结构:用 int 数组,避免 struct 对齐开销。
  4. 按秩合并用 dep 而非 rank:避免额外维护。
  5. find 中缓存根:减少重复查询。

6.4 优化后的迭代版本(关键优化)

见第 14 章完整模板。


7. 常见变形与用法

7.1 可持久化并查集 + 回退操作

题面中"回到第 k 次操作之后的状态"就是版本回退。
直接把当前版本号设为 \(k\),或把 root[i]=root[k]

7.2 强制在线

大部分可持久化题目强制在线,输入需异或 lastans

7.3 可持久化并查集 + 二分

配合可持久化版本,二分查找满足条件的最早版本。
例:最早使 \(a,b\) 连通的操作编号。

7.4 可持久化并查集 + 线段树合并

维护每个集合的额外信息(如权值和、大小),需另一棵可持久化线段树。

7.5 可持久化并查集 + 可持久化平衡树

用可持久化 Treap 代替主席树,支持分裂合并。
但常数较大,一般不推荐。

7.6 带权可持久化并查集

节点存 fadepw(到父节点的权)。
find 返回根与路径权值和。
用于可持久化带权并查集(如判断奇偶性)。

7.7 可持久化并查集 + 背包

每个版本是一个背包状态。
可用于动态连通性下的 DP。

7.8 可撤销并查集 vs 可持久化并查集

特性 可撤销 可持久化
回退 只能栈序回退 任意版本
路径压缩 可以(用栈记录) 不可以
复杂度 \(O(\log n)\) \(O(\log^2 n)\)
空间 \(O(m)\) \(O(m\log n)\)
在线 离线 在线

7.9 可持久化并查集 + 动态图连通性

动态图连通性可用线段树分治 + 可撤销并查集。
可持久化并查集处理"版本查询"型问题。

7.10 可持久化并查集 + Kruskal 重构树

Kruskal 重构树本身带版本信息,可看作可持久化并查集的一种变体。


8. 典型例题精讲

8.1 例 1:可持久化并查集(普通版)

题面
\(n\) 个集合,\(m\) 次操作:

  • 1 a b:合并 \(a,b\) 所在集合。
  • 2 k:回到第 \(k\) 次操作之后的状态。
  • 3 a b:询问 \(a,b\) 是否同集合。

输入样例

5 6
1 1 2
3 1 2
2 1
3 0 3
2 1
3 1 2

输出样例

1
0
1

解析

  • 建主席树存 fadep
  • find 不使用路径压缩,仅按秩合并。
  • 每次操作 root[i] 基于 root[i-1]
  • 操作 2 直接把 root[i]=root[k]

标准代码

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m,cnt;
int root[N],lc[N*40],rc[N*40],fa[N*40],dep[N*40];
void build(int &k,int l,int r)
{
    k=++cnt;
    if(l==r)
    {
        fa[k]=l;
        dep[k]=1;
        return;
    }
    int mid=l+r>>1;
    build(lc[k],l,mid);
    build(rc[k],mid+1,r);
}
void change(int pre,int &now,int l,int r,int pos,int f,int d)
{
    now=++cnt;
    lc[now]=lc[pre];
    rc[now]=rc[pre];
    if(l==r)
    {
        fa[now]=f;
        dep[now]=d;
        return;
    }
    int mid=l+r>>1;
    if(pos<=mid)change(lc[pre],lc[now],l,mid,pos,f,d);
    else change(rc[pre],rc[now],mid+1,r,pos,f,d);
}
pair<int,int>query(int k,int l,int r,int pos)
{
    if(l==r)return {fa[k],dep[k]};
    int mid=l+r>>1;
    if(pos<=mid)return query(lc[k],l,mid,pos);
    else return query(rc[k],mid+1,r,pos);
}
int find(int x,int i)
{
    while(1)
    {
        auto p=query(root[i],1,n,x);
        if(p.fi==x)return x;
        x=p.fi;
    }
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    build(root[0],1,n);
    for(int i=1;i<=m;i++)
    {
        int op;
        cin>>op;
        if(op==1)
        {
            int a,b;
            cin>>a>>b;
            root[i]=root[i-1];
            int x=find(a,i);
            int y=find(b,i);
            if(x==y)continue;
            auto px=query(root[i],1,n,x);
            auto py=query(root[i],1,n,y);
            if(px.se<py.se)swap(x,y),swap(px,py);
            int tmp;
            change(root[i],tmp,1,n,y,x,px.se);
            root[i]=tmp;
            if(px.se==py.se)
            {
                change(root[i],tmp,1,n,x,x,px.se+1);
                root[i]=tmp;
            }
        }
        else if(op==2)
        {
            int k;
            cin>>k;
            root[i]=root[k];
        }
        else
        {
            int a,b;
            cin>>a>>b;
            root[i]=root[i-1];
            int x=find(a,i);
            int y=find(b,i);
            cout<<(x==y?1:0)<<"\n";
        }
    }
    return 0;
}

8.2 例 2:可持久化并查集加强版

题面
同上,但强制在线:

  • \(a,b,k\) 均需异或 lastans
  • lastans 初值 0。
  • \(n,m\le2\cdot10^5\)

输入样例

5 6
1 1 2
3 1 2
2 1
3 0 3
2 1
3 1 2

输出样例

1
0
1

解析:用迭代版本 changequery_faquery_deep

修正后完整代码

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m,cnt;
int root[N*40],lc[N*40],rc[N*40],fa[N*40],deep[N*40];
int lastans=0;
void build(int &k,int l,int r)
{
    k=++cnt;
    if(l==r){fa[k]=l;deep[k]=1;return;}
    int mid=l+r>>1;
    build(lc[k],l,mid);
    build(rc[k],mid+1,r);
}
void change(int pre,int &now,int x,int y)
{
    now=++cnt;
    int l=1,r=n;
    int cur=now;
    while(l<r)
    {
        int mid=l+r>>1;
        if(y<=mid)
        {
            rc[cur]=rc[pre];
            lc[cur]=++cnt;
            cur=lc[cur];
            pre=lc[pre];
            r=mid;
        }
        else
        {
            lc[cur]=lc[pre];
            rc[cur]=++cnt;
            cur=rc[cur];
            pre=rc[pre];
            l=mid+1;
        }
    }
    fa[cur]=x;
    deep[cur]=deep[pre];
}
int query_fa(int node,int w)
{
    int l=1,r=n;
    while(l<r)
    {
        int mid=l+r>>1;
        if(w<=mid){node=lc[node];r=mid;}
        else{node=rc[node];l=mid+1;}
    }
    return fa[node];
}
int query_deep(int node,int w)
{
    int l=1,r=n;
    while(l<r)
    {
        int mid=l+r>>1;
        if(w<=mid){node=lc[node];r=mid;}
        else{node=rc[node];l=mid+1;}
    }
    return deep[node];
}
int find(int x,int i)
{
    while(1)
    {
        int p=query_fa(root[i],x);
        if(p==x)return x;
        x=p;
    }
}
void update(int pre,int &now,int w)
{
    now=++cnt;
    int l=1,r=n;
    int cur=now;
    while(l<r)
    {
        int mid=l+r>>1;
        if(w<=mid)
        {
            rc[cur]=rc[pre];
            lc[cur]=++cnt;
            cur=lc[cur];
            pre=lc[pre];
            r=mid;
        }
        else
        {
            lc[cur]=lc[pre];
            rc[cur]=++cnt;
            cur=rc[cur];
            pre=rc[pre];
            l=mid+1;
        }
    }
    fa[cur]=fa[pre];
    deep[cur]=deep[pre]+1;
}
void unionn(int x,int y,int i)
{
    int dx=query_deep(root[i],x);
    int dy=query_deep(root[i],y);
    if(dx<dy)swap(x,y);
    change(root[i-1],root[i],x,y);
    if(dx==dy)update(root[i],root[i],x);
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    build(root[0],1,n);
    int z,x,y;
    for(int i=1;i<=m;i++)
    {
        cin>>z;
        if(z==1)
        {
            cin>>x>>y;
            x^=lastans;
            y^=lastans;
            root[i]=root[i-1];
            x=find(x,i);
            y=find(y,i);
            if(x!=y)unionn(x,y,i);
        }
        else if(z==2)
        {
            cin>>x;
            x^=lastans;
            root[i]=root[x];
        }
        else
        {
            cin>>x>>y;
            x^=lastans;
            y^=lastans;
            root[i]=root[i-1];
            x=find(x,i);
            y=find(y,i);
            int ans=(x==y);
            cout<<ans<<"\n";
            lastans=ans;
        }
    }
    return 0;
}

注意:上述代码在样例上正确,但大数组 TLE 时需进一步优化(见第 11 章)。


9. 高级话题与研究扩展

9.1 可持久化并查集 + 二分答案

问题:给 \(m\) 次合并操作,问最早第几次操作后 \(a,b\) 连通。
解法:可持久化并查集 + 二分版本号。
复杂度 \(O(\log m\cdot\log^2 n)\)

int solve(int a,int b,int l,int r)
{
    while(l<r)
    {
        int mid=l+r>>1;
        if(find(a,mid)==find(b,mid))r=mid;
        else l=mid+1;
    }
    return l;
}

9.2 可持久化并查集 + 线段树合并

维护每个集合的权值和、最大/最小等。
每个集合对应一棵动态开点线段树。
合并时线段树合并,并把新根存入可持久化数组。
复杂度 \(O(n\log n)\)

9.3 可持久化并查集 + 带权

节点存 fadepw(到父节点权值)。
find 返回根与路径异或和。
常用于可持久化奇偶并查集。

pair<int,int>find(int x,int i)
{
    int w=0;
    while(1)
    {
        auto p=query(root[i],1,n,x);
        if(p.fi==x)return {x,w};
        w^=p.se;
        x=p.fi;
    }
}

9.4 可持久化并查集 + 哈希

用可持久化线段树存哈希,判断两版本是否相同。
用于可持久化字符串、可持久化图同构。

9.5 可持久化并查集 + 分块

把版本分块,块内用普通并查集,块间用可持久化。
适合版本数巨大但查询集中在某段。

9.6 可持久化并查集 + 随机化

用随机秩代替 dep,期望 \(O(\log n)\) 树高。
节省一次 change

9.7 可持久化并查集 + 可持久化 Treap

用可持久化 Treap 代替主席树。
支持分裂合并,常数较大。

9.8 可持久化并查集 + 动态树

Link-Cut Tree 也可维护连通性。
但 LCT 不支持任意版本回退。
可持久化 LCT 是研究课题。

9.9 可持久化并查集 + 可持久化栈

用可持久化栈代替数组,简化实现。
find 无法快速定位,需配合平衡树。

9.10 可持久化并查集 + 并行算法

多线程版本可持久化并查集。
研究热点,涉及并发控制。


10. 相关定理、推论、公式速查

10.1 按秩合并树高定理

定理:按秩合并的并查集,秩为 \(r\) 的树至少有 \(2^r\) 个节点。
推论:树高 \(\le\lfloor\log_2 n\rfloor\)

10.2 反阿克曼函数

\[\alpha(n)=\min\{k:A(k,k)\ge n\} \]

其中 \(A\) 为阿克曼函数。\(\alpha(n)\le4\)\(n<2^{65536}\))。

10.3 Tarjan 定理

路径压缩 + 按秩合并,摊还 \(O(\alpha(n))\)

10.4 可持久化复杂度定理

定理:可持久化并查集单次 union 时间 \(O(\log^2 n)\),空间 \(O(\log n)\)
证明find 每层主席树查询 \(O(\log n)\),树高 \(O(\log n)\),总 \(O(\log^2 n)\)。每次 change 新建 \(O(\log n)\) 节点。

10.5 主定理(Master Theorem)

\(T(n)=aT(n/b)+f(n)\),则:

  • \(f(n)=O(n^{\log_b a-\epsilon})\)\(T(n)=\Theta(n^{\log_b a})\)
  • \(f(n)=\Theta(n^{\log_b a})\)\(T(n)=\Theta(n^{\log_b a}\log n)\)
  • \(f(n)=\Omega(n^{\log_b a+\epsilon})\)\(T(n)=\Theta(f(n))\)

用于分析线段树复杂度。

10.6 空间-时间权衡

结构 空间 时间
普通并查集 \(O(n)\) \(O(\alpha(n))\)
可撤销并查集 \(O(m)\) \(O(\log n)\)
可持久化并查集 \(O(m\log n)\) \(O(\log^2 n)\)
带权可持久化 \(O(m\log n)\) \(O(\log^2 n)\)

10.7 主席树节点数估算

\[\text{nodes}\le n+4m\log_2 n \]

对于 \(n,m=2\cdot10^5\),约 \(1.5\cdot10^7\)

10.8 编码技巧公式

  • 左子:k<<1lc[k]
  • 中点:l+r>>1
  • 异或在线:x^=lastans
  • 版本根:root[i]
  • 节点计数:++cnt

11. 代码优化技巧

11.1 迭代代替递归

递归函数调用开销大。
changequeryfindupdate 全改为迭代。

11.2 数组连续化

int 数组而非 struct,避免对齐开销。
fa[]dep[]lc[]rc[] 分开。

11.3 内存池

预分配大数组,用 cnt 计数。
避免 vector 扩容。

11.4 快读快写

对于 \(2\cdot10^5\) 数据,cinsync_with_stdio(false) 足够。
若更大,用 getchar 快读。

11.5 减少 change 次数

union 时若能一次修改 fadep,减少节点。
用一个 change 修改双值。

11.6 缓存 query 结果

find 中每次 query 都是 \(O(\log n)\)
若同一版本多次 find,可缓存。

11.7 优化 find

find 中若 x 是根,直接返回,避免一次 query

11.8 二进制优化

lcrcint 而非 long long
#define int long long 会浪费空间,但方便。

11.9 迭代式 change 模板

void change(int pre,int &now,int x,int y)
{
    now=++cnt;
    int l=1,r=n,cur=now;
    while(l<r)
    {
        int mid=l+r>>1;
        if(y<=mid)
        {
            rc[cur]=rc[pre];
            lc[cur]=++cnt;
            cur=lc[cur];
            pre=lc[pre];
            r=mid;
        }
        else
        {
            lc[cur]=lc[pre];
            rc[cur]=++cnt;
            cur=rc[cur];
            pre=rc[pre];
            l=mid+1;
        }
    }
    fa[cur]=x;
    deep[cur]=deep[pre];
}

11.10 迭代式 query

int query_fa(int node,int w)
{
    int l=1,r=n;
    while(l<r)
    {
        int mid=l+r>>1;
        if(w<=mid){node=lc[node];r=mid;}
        else{node=rc[node];l=mid+1;}
    }
    return fa[node];
}

11.11 迭代式 find

int find(int x,int i)
{
    while(1)
    {
        int p=query_fa(root[i],x);
        if(p==x)return x;
        x=p;
    }
}

11.12 内联函数

inline#define 宏,减少调用开销。

11.13 优化后完整代码(推荐用于加强版)

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m,cnt;
int root[N],lc[N*40],rc[N*40],fa[N*40],deep[N*40];
int lastans=0;
inline void build(int &k,int l,int r)
{
    k=++cnt;
    if(l==r){fa[k]=l;deep[k]=1;return;}
    int mid=l+r>>1;
    build(lc[k],l,mid);
    build(rc[k],mid+1,r);
}
inline void change(int pre,int &now,int x,int y)
{
    now=++cnt;
    int l=1,r=n,cur=now;
    while(l<r)
    {
        int mid=l+r>>1;
        if(y<=mid)
        {
            rc[cur]=rc[pre];
            lc[cur]=++cnt;
            cur=lc[cur];
            pre=lc[pre];
            r=mid;
        }
        else
        {
            lc[cur]=lc[pre];
            rc[cur]=++cnt;
            cur=rc[cur];
            pre=rc[pre];
            l=mid+1;
        }
    }
    fa[cur]=x;
    deep[cur]=deep[pre];
}
inline int query_fa(int node,int w)
{
    int l=1,r=n;
    while(l<r)
    {
        int mid=l+r>>1;
        if(w<=mid){node=lc[node];r=mid;}
        else{node=rc[node];l=mid+1;}
    }
    return fa[node];
}
inline int query_deep(int node,int w)
{
    int l=1,r=n;
    while(l<r)
    {
        int mid=l+r>>1;
        if(w<=mid){node=lc[node];r=mid;}
        else{node=rc[node];l=mid+1;}
    }
    return deep[node];
}
inline int find(int x,int i)
{
    while(1)
    {
        int p=query_fa(root[i],x);
        if(p==x)return x;
        x=p;
    }
}
inline void update(int pre,int &now,int w)
{
    now=++cnt;
    int l=1,r=n,cur=now;
    while(l<r)
    {
        int mid=l+r>>1;
        if(w<=mid)
        {
            rc[cur]=rc[pre];
            lc[cur]=++cnt;
            cur=lc[cur];
            pre=lc[pre];
            r=mid;
        }
        else
        {
            lc[cur]=lc[pre];
            rc[cur]=++cnt;
            cur=rc[cur];
            pre=rc[pre];
            l=mid+1;
        }
    }
    fa[cur]=fa[pre];
    deep[cur]=deep[pre]+1;
}
inline void unionn(int x,int y,int i)
{
    int dx=query_deep(root[i],x);
    int dy=query_deep(root[i],y);
    if(dx<dy)swap(x,y);
    change(root[i-1],root[i],x,y);
    if(dx==dy)update(root[i],root[i],x);
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    build(root[0],1,n);
    int z,x,y;
    for(int i=1;i<=m;i++)
    {
        cin>>z;
        if(z==1)
        {
            cin>>x>>y;
            x^=lastans;
            y^=lastans;
            root[i]=root[i-1];
            x=find(x,i);
            y=find(y,i);
            if(x!=y)unionn(x,y,i);
        }
        else if(z==2)
        {
            cin>>x;
            x^=lastans;
            root[i]=root[x];
        }
        else
        {
            cin>>x>>y;
            x^=lastans;
            y^=lastans;
            root[i]=root[i-1];
            x=find(x,i);
            y=find(y,i);
            int ans=(x==y);
            cout<<ans<<"\n";
            lastans=ans;
        }
    }
    return 0;
}

11.14 双值一次修改优化

fadep 的修改合并为一次 change

inline void change(int pre,int &now,int pos,int f,int d)
{
    now=++cnt;
    int l=1,r=n,cur=now;
    while(l<r)
    {
        int mid=l+r>>1;
        if(pos<=mid)
        {
            rc[cur]=rc[pre];
            lc[cur]=++cnt;
            cur=lc[cur];
            pre=lc[pre];
            r=mid;
        }
        else
        {
            lc[cur]=lc[pre];
            rc[cur]=++cnt;
            cur=rc[cur];
            pre=rc[pre];
            l=mid+1;
        }
    }
    fa[cur]=f;
    deep[cur]=d;
}

union 中:

int dx=query_deep(root[i],x);
int dy=query_deep(root[i],y);
if(dx<dy)swap(x,y),swap(dx,dy);
change(root[i-1],root[i],y,x,dx);
if(dx==dy)change(root[i],root[i],x,x,dx+1);

11.15 进一步优化:find 传根

避免重复查根:

int find_root(int x,int i)
{
    while(1)
    {
        int p=query_fa(root[i],x);
        if(p==x)return x;
        x=p;
    }
}

11.16 减少 query_deep 调用

union 时若已知 x,y 是根,可直接查 dep

11.17 使用 unsigned 优化

节点编号用 int,其他用 int 即可。

11.18 空间释放

若版本数超过 \(m\),需释放旧版本。
但可持久化语义要求保留所有版本。

11.19 二分优化

二分版本时,从根节点二分可减少常数。

11.20 编译器优化

-O2-O3 开启。
#pragma GCC optimize("O3")


12. 与其他可持久化结构结合

12.1 可持久化线段树 + 可持久化并查集

最常见组合,已述。

12.2 可持久化 Trie + 并查集

Trie 存字符串集合,并查集维护等价关系。
可用于可持久化字符串归并。

12.3 可持久化平衡树 + 并查集

Treap 代替主席树,支持分裂合并。
适合动态插入删除。

12.4 可持久化栈 + 并查集

栈存版本历史,适合"回退"型问题。

12.5 可持久化队列 + 并查集

双栈模拟队列,用于 BFS 可持久化。

12.6 可持久化图 + 并查集

可持久化邻接表,用于动态图连通性。

12.7 可持久化哈希 + 并查集

判断两版本是否同构。

12.8 可持久化 Kruskal 重构树

把每次合并建新节点,形成树。
可持久化版本对应子树。

12.9 可持久化动态树

LCT + 可持久化,研究课题。

12.10 可持久化 2-SAT

并查集 + 2-SAT 结合,用于可持久化约束。


13. 常见错误与调试

13.1 头文件拼写错误

<bids/stdc++.h> 应为 <bits/stdc++.h>不然会被卡头文件

13.2 数组开小

  • N*20 不够,应为 N*40 或更大。
  • 对于 \(n,m=2\cdot10^5\)N*40=8\cdot10^6 足够。

13.3 未处理强制在线

忘记 x^=lastans
忘记更新 lastans

13.4 混用元素编号与节点编号

deep[x]x 是元素编号,应改为 query_deep(root[i],x)

13.5 change 不可持久化

直接修改旧节点,破坏历史版本。
now=++cnt,复制路径。

13.6 update 错误

应复制 fa,且 deep 加一。
update(root[i],root[i],x) 应把新根赋给 root[i]

13.7 路径压缩误用

可持久化并查集不能用路径压缩。
应用按秩合并。

13.8 递归超时

递归 find + query 嵌套,常数巨大。
改为迭代。

13.9 版本号错位

root[i] 应基于 root[i-1]
操作 2 直接 root[i]=root[k]

13.10 union 未判断同集合

x==y 直接跳过。
否则可能错误修改。

13.11 合并方向错误

按秩合并:小秩连大秩。
dep 相等时,被连的树 dep 加一。

13.12 节点编号溢出

cntint,最大 \(8\cdot10^6\),不溢出。
若更大,用 long long

13.13 内存超限

减少节点数:

  • 双值一次修改。
  • int 而非 long long
  • 释放不用版本。

13.14 调试方法

  • 打印每个版本的 fadep
  • 对比暴力。
  • 检查节点数。
  • 检查版本根。

14. 完整模板代码

14.1 可持久化并查集(普通版)

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m,cnt;
int root[N],lc[N*40],rc[N*40],fa[N*40],dep[N*40];
void build(int &k,int l,int r)
{
    k=++cnt;
    if(l==r){fa[k]=l;dep[k]=1;return;}
    int mid=l+r>>1;
    build(lc[k],l,mid);
    build(rc[k],mid+1,r);
}
void change(int pre,int &now,int l,int r,int pos,int f,int d)
{
    now=++cnt;
    lc[now]=lc[pre];
    rc[now]=rc[pre];
    if(l==r){fa[now]=f;dep[now]=d;return;}
    int mid=l+r>>1;
    if(pos<=mid)change(lc[pre],lc[now],l,mid,pos,f,d);
    else change(rc[pre],rc[now],mid+1,r,pos,f,d);
}
pair<int,int>query(int k,int l,int r,int pos)
{
    if(l==r)return {fa[k],dep[k]};
    int mid=l+r>>1;
    if(pos<=mid)return query(lc[k],l,mid,pos);
    else return query(rc[k],mid+1,r,pos);
}
int find(int x,int i)
{
    while(1)
    {
        auto p=query(root[i],1,n,x);
        if(p.fi==x)return x;
        x=p.fi;
    }
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    build(root[0],1,n);
    for(int i=1;i<=m;i++)
    {
        int op;
        cin>>op;
        if(op==1)
        {
            int a,b;
            cin>>a>>b;
            root[i]=root[i-1];
            int x=find(a,i);
            int y=find(b,i);
            if(x==y)continue;
            auto px=query(root[i],1,n,x);
            auto py=query(root[i],1,n,y);
            if(px.se<py.se)swap(x,y),swap(px,py);
            int tmp;
            change(root[i],tmp,1,n,y,x,px.se);
            root[i]=tmp;
            if(px.se==py.se)
            {
                change(root[i],tmp,1,n,x,x,px.se+1);
                root[i]=tmp;
            }
        }
        else if(op==2)
        {
            int k;
            cin>>k;
            root[i]=root[k];
        }
        else
        {
            int a,b;
            cin>>a>>b;
            root[i]=root[i-1];
            int x=find(a,i);
            int y=find(b,i);
            cout<<(x==y?1:0)<<"\n";
        }
    }
    return 0;
}

14.2 可持久化并查集(加强版,迭代优化)

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m,cnt;
int root[N],lc[N*40],rc[N*40],fa[N*40],deep[N*40];
int lastans=0;
inline void build(int &k,int l,int r)
{
    k=++cnt;
    if(l==r){fa[k]=l;deep[k]=1;return;}
    int mid=l+r>>1;
    build(lc[k],l,mid);
    build(rc[k],mid+1,r);
}
inline void change(int pre,int &now,int x,int y)
{
    now=++cnt;
    int l=1,r=n,cur=now;
    while(l<r)
    {
        int mid=l+r>>1;
        if(y<=mid)
        {
            rc[cur]=rc[pre];
            lc[cur]=++cnt;
            cur=lc[cur];
            pre=lc[pre];
            r=mid;
        }
        else
        {
            lc[cur]=lc[pre];
            rc[cur]=++cnt;
            cur=rc[cur];
            pre=rc[pre];
            l=mid+1;
        }
    }
    fa[cur]=x;
    deep[cur]=deep[pre];
}
inline int query_fa(int node,int w)
{
    int l=1,r=n;
    while(l<r)
    {
        int mid=l+r>>1;
        if(w<=mid){node=lc[node];r=mid;}
        else{node=rc[node];l=mid+1;}
    }
    return fa[node];
}
inline int query_deep(int node,int w)
{
    int l=1,r=n;
    while(l<r)
    {
        int mid=l+r>>1;
        if(w<=mid){node=lc[node];r=mid;}
        else{node=rc[node];l=mid+1;}
    }
    return deep[node];
}
inline int find(int x,int i)
{
    while(1)
    {
        int p=query_fa(root[i],x);
        if(p==x)return x;
        x=p;
    }
}
inline void update(int pre,int &now,int w)
{
    now=++cnt;
    int l=1,r=n,cur=now;
    while(l<r)
    {
        int mid=l+r>>1;
        if(w<=mid)
        {
            rc[cur]=rc[pre];
            lc[cur]=++cnt;
            cur=lc[cur];
            pre=lc[pre];
            r=mid;
        }
        else
        {
            lc[cur]=lc[pre];
            rc[cur]=++cnt;
            cur=rc[cur];
            pre=rc[pre];
            l=mid+1;
        }
    }
    fa[cur]=fa[pre];
    deep[cur]=deep[pre]+1;
}
inline void unionn(int x,int y,int i)
{
    int dx=query_deep(root[i],x);
    int dy=query_deep(root[i],y);
    if(dx<dy)swap(x,y);
    change(root[i-1],root[i],x,y);
    if(dx==dy)update(root[i],root[i],x);
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    build(root[0],1,n);
    int z,x,y;
    for(int i=1;i<=m;i++)
    {
        cin>>z;
        if(z==1)
        {
            cin>>x>>y;
            x^=lastans;
            y^=lastans;
            root[i]=root[i-1];
            x=find(x,i);
            y=find(y,i);
            if(x!=y)unionn(x,y,i);
        }
        else if(z==2)
        {
            cin>>x;
            x^=lastans;
            root[i]=root[x];
        }
        else
        {
            cin>>x>>y;
            x^=lastans;
            y^=lastans;
            root[i]=root[i-1];
            x=find(x,i);
            y=find(y,i);
            int ans=(x==y);
            cout<<ans<<"\n";
            lastans=ans;
        }
    }
    return 0;
}

14.3 带权可持久化并查集

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m,cnt;
int root[N],lc[N*40],rc[N*40],fa[N*40],dep[N*40],w[N*40];
void build(int &k,int l,int r)
{
    k=++cnt;
    if(l==r){fa[k]=l;dep[k]=1;w[k]=0;return;}
    int mid=l+r>>1;
    build(lc[k],l,mid);
    build(rc[k],mid+1,r);
}
void change(int pre,int &now,int l,int r,int pos,int f,int d,int ww)
{
    now=++cnt;
    lc[now]=lc[pre];
    rc[now]=rc[pre];
    if(l==r){fa[now]=f;dep[now]=d;w[now]=ww;return;}
    int mid=l+r>>1;
    if(pos<=mid)change(lc[pre],lc[now],l,mid,pos,f,d,ww);
    else change(rc[pre],rc[now],mid+1,r,pos,f,d,ww);
}
pair<pair<int,int>,int>query(int k,int l,int r,int pos)
{
    if(l==r)return {{fa[k],dep[k]},w[k]};
    int mid=l+r>>1;
    if(pos<=mid)return query(lc[k],l,mid,pos);
    else return query(rc[k],mid+1,r,pos);
}
pair<int,int>find(int x,int i)
{
    int ww=0;
    while(1)
    {
        auto p=query(root[i],1,n,x);
        if(p.fi.fi==x)return {x,ww};
        ww^=p.se;
        x=p.fi.fi;
    }
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    
    cin>>n>>m;
    build(root[0],1,n);
    // 略:类似加强版,union 时更新 w
    return 0;
}

14.4 可持久化并查集二分版本

// 在 14.2 基础上加二分
int solve(int a,int b,int l,int r)
{
    while(l<r)
    {
        int mid=l+r>>1;
        if(find(a,mid)==find(b,mid)) r=mid;
        else l=mid+1;
    }
    return l;
}

15. 公式、定理、推论汇总表

名称 内容
按秩合并树高 \(\le\lfloor\log_2 n\rfloor\)
反阿克曼 \(\alpha(n)\le4\)
Tarjan 定理 路径压缩+按秩合并摊还 \(O(\alpha(n))\)
可持久化 union \(O(\log^2 n)\) 时间,\(O(\log n)\) 空间
主席树节点数 \(n+4m\log_2 n\)
主定理 \(T(n)=aT(n/b)+f(n)\)
路径复制 每次 \(O(\log n)\) 新节点
版本回退 \(O(1)\)
异或在线 \(x\gets x\oplus\text{lastans}\)

16. 结语

可持久化并查集是并查集与可持久化数据结构结合的经典产物。
要点:

  1. 不能用路径压缩,只能用按秩合并。
  2. 用可持久化线段树存 fadep
  3. 每次操作基于上一版本,产生新根。
  4. 版本回退 \(O(1)\)
  5. 强制在线需异或 lastans
  6. 迭代实现比递归快很多。
  7. 双值一次修改可省一半节点。
  8. 数组要开到 N*40
  9. 常用于二分版本、可持久化连通性、动态图问题。

建议刷题顺序:

  1. 模板题:可持久化并查集。
  2. 加强版:强制在线 + 迭代优化。
  3. 带权可持久化并查集。
  4. 可持久化并查集 + 二分。
  5. 可持久化并查集 + 线段树合并。
posted @ 2026-09-19 18:36  shenchen2011  阅读(4)  评论(0)    收藏  举报