可持久化并查集

可持久化并查集


题目描述

洛谷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
#include
#include
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

你可能感兴趣的:(算法进阶,可持久化并查集)