可持久化并查集

可持久化并查集


题目描述

洛谷P3402 可持久化并查集


核心思路

可持久化并查集是建立在可持久化数组上的,在学习可持久化并查集之前,需要先学习主席树(可持久化权值线段树),权值线段树,可持久化线段树,移步可持久化线段树1可持久化线段树2

可持久化并查集=可持久化+并查集=可持久化数组+并查集=主席树+并查集

并查集有两种优化方式:

  • 路径压缩
  • 按秩合并

由于需要我们支持的只有集合的合并、查询操作,当我们需要将两个集合合二为一时,无论将哪一个集合连接到另一个集合的下面,都能得到正确的结果。但不同的连接方法存在时间复杂度的差异。具体来说,如果我们将一棵点数与深度都较小的集合树连接到一棵更大的集合树下,显然相比于另一种连接方案,接下来执行查找操作的用时更小。

如果并查集退化成一条链,那么查询find的时间复杂度就会很高了。所以,我们这里采用按秩合并,即把深度小的合并到深度大的,这样就保持了深度均衡。由于路径压缩单次合并可能造成大量修改,有时路径压缩并不适合使用。例如,在可持久化并查集、线段树分治 + 并查集中,一般使用只启发式合并的并查集。也就是说,我们这道题并不能使用路径压缩。

我们的并查集的fa数组是要用可持久化数组来维护的,那么路径压缩基本就不要考虑了。为什么不能考虑路径压缩呢?

我们来看路径压缩的代码:

int find(int x)
{
    if(x!=fa[x])
        fa[x]=find(fa[x]);
    return fa[x];
}

每递归一次,只要进入if语句,fa数组就会有一个位置被修改, 对于普通数组来说,这修改完全没问题,但是不要忘了对于建立在主席树上的可持久化数组每进行一次单点修改就会多一个新的版本存放新的结点。小数据暂且无妨,但是大数据妥妥MLE, 所以路径压缩就不要使用了。

因为每个版本的并查集的结点深度可能是不一样的,所以我们还需要要新开一个可持久化数组来记录每个版本的dep数组

因此对于这道题,可持久化并查集其实就是指的用可持久化数组维护并查集中的fa[]和维护按秩合并所需要的dep[]。 用两个可持久化数组分别维护并查集的fa数组(每个集合的根结点)和dep数组(每个结点的深度), 并查集要按秩合并,不要路径压缩, 这样就好啦。

所谓可持久化并查集,可以进行的操作就只有几个:

  • 合并两个集合(毕竟还是个并查集呀)
  • 回到历史版本(不然怎么叫可持久化呢)
  • 查询节点所在集合的祖先,当然,因此也可以判断是否在同一个集合中(毕竟还是个并查集呀)

对于操作2,我们可以很容易的使用可持久化数组来实现。就直接把当前版本的根节点定为第k个版本的根节点就行了,即 r o o t f a [ i ] = r o o t f a [ k ] rootfa[i]=rootfa[k] rootfa[i]=rootfa[k] r o o t d e p [ i ] = r o o t d e p [ k ] rootdep[i]=rootdep[k] rootdep[i]=rootdep[k]

对于操作1,也就是上面所说的按秩合并。对于操作3,也就是在可持久化数组中查询。

怎样开两个可持久化数组(主席树)记录fa和dep?

我们可以建立一个双倍大小的内存池,然后建立两个根节点数组rootfa[]rootdep[]来维护这两个可持久化数组(主席树)的根节点,而内存池的计数器还是就那一个idx就够了。也就是,你要开几个主席树,那么就开多少个root数组。

两个可持久化数组是否需要先构建出来,即是否先进行build建树操作?

对于rootfa[]数组来说是需要的,但是对于rootdep[]数组来说是没有必要的。因为初始时作为并查集的fa数组是要初始化的,即每个节点都独立成一个集合,因此需要建立出来。 对于dep数组来说,初始时所有结点的深度都为0,又由于我们定义该数组为全局的,默认为0,所以并不需要建立出来,这与权值线段树都是同样的道理


代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=2e5+10;
struct Node{
    int l,r,val;
}tr[N*40*2];
//idx负责给树中的每个结点分配一个编号
//tot负责给树中的每个结点赋一个值
//rootfa[i]记录的是第i个版本的并查集中的集合号(祖宗结点)
//rootdep[i]记录的是第i个版本的并查集中的集合号的深度(祖宗结点的深度)
int idx,tot,rootfa[N],rootdep[N];
int n,m;

void build(int l,int r,int &u)
{
    u=++idx;    //给结点u分配一个编号
    if(l==r)
    {   
        tr[u].val=++tot;    //给结点u赋一个值
        return;
    }
    int mid=(l+r)>>1;
    build(l,mid,tr[u].l);   //递归创建左子树
    build(mid+1,r,tr[u].r); //递归创建右子树
}

void modify(int l,int r,int ver,int &u,int pos,int val)
{
    u=++idx;    //给当前节点u分配一个编号
    //当前版本u继承上一个版本ver
    tr[u]=tr[ver];
    if(l==r)//到了叶子结点
    {
        tr[u].val=val;  //将结点u的值修改为val
        return;
    }
    int mid=(l+r)>>1;
    if(pos<=mid)    //递归去修改左子树
        modify(l,mid,tr[ver].l,tr[u].l,pos,val);
    else            //递归去修改右子树
        modify(mid+1,r,tr[ver].r,tr[u].r,pos,val);
}

int query(int l,int r,int u,int pos)
{
    if(l==r)//到了叶子结点 返回该节点的值
        return tr[u].val;
    int mid=(l+r)>>1;
    if(pos<=mid)    //递归查询左子树
        return query(l,mid,tr[u].l,pos);
    else            //递归查询右子树
        return query(mid+1,r,tr[u].r,pos);
}

//要找到第ver个版本中x的父亲
int find(int ver,int x)
{
    //去第ver个版本的线段树(其根节点是rootfa[ver])中查询位置x上的值
    int fx=query(1,n,rootfa[ver],x);
    if(x!=fx)
        x=find(ver,fx); //不能进行路径压缩
    return x;
}

//将第ver个版本中的集合x和集合y
void merge(int ver,int x,int y)
{
    //传进来的ver是一个全新的版本 我们在op=1时执行了merge操作
    //比如ver=0,我们已经构建好了 然后传进来ver=1 
    //执行merge操作那么此时ver=1这个全新的版本中并没有任何东西 
    //因为我们在执行merge之前并没有弄过rootfa[ver]=rootfa[ver-1] 所以它现在是空的
    //因此如果我们想要寻找当前ver版本中结点x的父亲 那么可以去上一个版本ver-1中找x的父亲
    x=find(ver-1,x);    //找到x所在的集合号
    y=find(ver-1,y);    //找到y所在的集合号
    if(x==y)//在同一个集合中
    {
        //当前版本直接继承上一个版本即可
        //其实就是虽然没有把x和y合并  但是由于我们执行了merge这次操作
        //既然是操作 那么就会产生新版本  所以需要当前版本直接继承上一个版本
        rootfa[ver]=rootfa[ver-1];
        rootdep[ver]=rootdep[ver-1];
    }
    else
    {
        int depx=query(1,n,rootdep[ver-1],x);   //求出x的深度
        int depy=query(1,n,rootdep[ver-1],y);   //求出y的深度
        if(depx<depy)
        {
            //将深度小的x合并到深度大的y上   含义就是fa[x]=y
            modify(1,n,rootfa[ver-1],rootfa[ver],x,y);
            //将深度小的x合并到深度大的y上不会导入深度有变化
            //因此当前版本的深度直接继承上一个版本的深度即可
            rootdep[ver]=rootdep[ver-1];
        }
        else if(depy<depx)
        {
            //将深度小的y合并到深度大的x上   含义就是fa[y]=x
            modify(1,n,rootfa[ver-1],rootfa[ver],y,x);
            //将深度小的x合并到深度大的y上不会导入深度有变化
            //因此当前版本的深度直接继承上一个版本的深度即可
            rootdep[ver]=rootdep[ver-1];
        }
        else    //这两个集合的深度相等
        {
            //将x合并到y
            modify(1,n,rootfa[ver-1],rootfa[ver],x,y);
            //此时y的深度多1
            modify(1,n,rootdep[ver-1],rootdep[ver],y,depy+1);
        }
        
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    build(1,n,rootfa[0]);
    for(int i=1;i<=m;i++)
    {
        int op,a,b;
        scanf("%d",&op);
        if(op==1)   //合并两个集合a,b
        {
            scanf("%d%d",&a,&b);
            merge(i,a,b);
        }
        else if(op==2)//回到第k次操作
        {
            int k;
            scanf("%d",&k);
            //将当前版本追溯到第k个版本
            rootfa[i]=rootfa[k];
            rootdep[i]=rootdep[k];
        }
        else if(op==3)//询问a,b是否在同一个集合中
        {
            scanf("%d%d",&a,&b);
            //询问也是一次操作 但是它没有对上一个版本进行修改
            //所以执行询问时 也算一次操作  那么当前版本直接继承上一个版本即可
            rootfa[i]=rootfa[i-1];
            rootdep[i]=rootdep[i-1];
            a=find(i,a);
            b=find(i,b);
            if(a==b)
                puts("1");
            else
                puts("0");
        }
    }
    return 0;
}

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
持久化并查集是指在并查集的基础上,支持回退到任意历史版本。这个结构可以用来处理一些需要撤销或者回退操作的问题。以下是一个基本的可持久化并查集的实现。 ```python class Node: def __init__(self, parent=None, rank=0): self.parent = parent self.rank = rank class PersistentUnionFind: def __init__(self, size): self.n = size self.roots = [None] * (2 * size) self.ranks = [None] * (2 * size) def make_set(self, v): self.roots[v] = Node(v) self.ranks[v] = 0 def find(self, node, version): if node.parent is None: return node if node.parent != node: node.parent = self.find(node.parent, version) return node.parent def union(self, x, y, version): x_root = self.find(self.roots[x], version) y_root = self.find(self.roots[y], version) if x_root == y_root: return False if self.ranks[x_root] < self.ranks[y_root]: x_root, y_root = y_root, x_root new_root = Node(x_root, self.ranks[x_root] + (self.ranks[x_root] == self.ranks[y_root])) self.roots[x] = self.roots[y] = new_root self.ranks[x_root] = self.ranks[y_root] = new_root.rank return True def get_version(self): return len(self.roots) // self.n - 1 def get_root(self, v, version): return self.find(self.roots[v], version).parent.val ``` 这个代码中,我们使用了一个 `Node` 类来表示每个节点,其中 `parent` 表示节点的父亲,`rank` 表示节点的秩。我们需要用一个 `roots` 数组来保存所有版本的根节点,以及一个 `ranks` 数组来保存所有节点的秩。`make_set` 函数用来初始化一个新节点,这个节点的父亲指向自己,秩为 0。`find` 函数用来找到节点所在的集合的根节点。如果节点的父亲不是根节点,那么我们就递归地寻找它的父亲。在递归返回之前,我们将所有遍历过的节点的父亲都更新为根节点,这样可以加速下次查找。`union` 函数用来将两个节点所在的集合合并。首先找到两个节点所在集合的根节点,如果根节点相同,那么这两个节点已经在同一个集合中,不需要再次合并。否则,我们将秩较小的根节点挂在秩较大的根节点下面,同时更新秩。`get_version` 函数用来获取当前版本号,而 `get_root` 函数则用来获取节点在指定版本中所在的集合的根节点。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值