zoukankan      html  css  js  c++  java
  • 数据结构——可持久化并查集

    前置知识

    可持久化数组

    简介

    先来一道模板题:

    可持久化并查集

    大致意思就是要你写一个数据结构,支持

    1. 合并a,b所在集合

    2. 退回到第k次操作之后的状态

    3. 查询a和b是不是在同一个集合里面

    可以看到,除去第二个操作以外普通的并查集就可以解决

    普通并查集一般是基于数组的,而可持久化并查集是基于可持久化数组的

    话说这个东西名字听起来很高端,实际上实现起来其实不是很难(

    并查集

    相信都会写普通的并查集。。。但是这里有一点比较重要

    普通的并查集最简单的优化是路径压缩和按秩合并(我比较菜所以一般只打路径压缩),就是这两种优化让并查集的时间复杂度变成近似于常数,但是可持久化并查集不能用路径压缩。因为路径压缩的过程会对fa数组进行修改,普通的数组改一改还ok,但是可持久化数组的每次更改都会添加一条链。久而久之整个并查集的空间复杂度就会起飞,所以我们主要使用按秩合并的思路。

    按秩合并的话其实也不难,这里给出一份普通并查集的按秩合并代码:

    void merge(int x,int y) {
    	x=find(x);
    	y=find(y);
    	if(x==y) return ;
    	if(dep[x]<dep[y])
    		fa[x]=y;
    	else {
    		fa[y]=x;
    		if(dep[x]==dep[y]) dep[x]++;
    	}
    }
    

    就是在合并的时候只从深度小的往深度大的合并,然后深度一样的话就特判之后dep[x]++就好了,这样可以保证树高最高为log(n),也就是不存在链。

    具体的原理。。。就自行百度吧,反正也不是很难(和启发式合并有点像?)

    可持久化并查集

    因为我们的优化是按秩合并,所以我们要维护两个可持久化数组(fa和dep)。当然,既然是两个数组,我们就要开两倍的内存空间:

    struct node {
    	int l,r,sum;
    } t[maxn*40*2];
    

    然后我们通过开两个root数组来标记不同的数组的树根,这样就相当于开两个可持久化数组了,然后加上用来分配内存的计数器和一些题目里面的变量什么的

    int n,m,tot,cnt,rootfa[maxn],rootdep[maxn];
    

    接着,并查集一开始有一个给fa数组赋值的操作,fa[i]=i,所以我们在可持久化数里面也写一个build函数来完成这个工作

    void build(int l,int r,int &now) {
    	now=++cnt;
    	if(l==r) {
    		t[now].sum=++tot;
    		return;
    	}
    	int mid=(l+r)/2;
    	build(l,mid,t[now].l);
    	build(mid+1,r,t[now].r);
    }
    

    这里的tot就是上面定义的,用来给叶子节点自增 ,应该还是蛮简单的

    接下来就是一个可持久化数组的板子,这里就不再赘述了:

    void modify(int l,int r,int ver,int &now,int pos,int num) {//ver指向历史版本,now指向当前节点
    	t[now=++cnt]=t[ver];
    	if(l==r) {
    		t[now].sum=num;
    		return;
    	}
    	int mid=(l+r)/2;
    	if(pos<=mid) modify(l,mid,t[ver].l,t[now].l,pos,num);
    	else modify(mid+1,r,t[ver].r,t[now].r,pos,num);
    }
    int query(int l,int r,int now,int pos) {
    	if(l==r) return t[now].sum;
    	int mid=(l+r)/2;
    	if(pos<=mid) return query(l,mid,t[now].l,pos);
    	else return query(mid+1,r,t[now].r,pos);
    }
    

    然后我们就来写find函数了。

    find函数本身还是比较简单,但是要注意,我们不要路径压缩。原因前面已经讲了,这里就直接放代码:

    int find(int ver,int x) {
    	int fx=query(1,n,rootfa[ver],x);
    	return fx==x?x:find(ver,fx);
    }
    

    然后我们先不急着说merge函数,我们先来看一下2操作是怎么实现的。

    2操作是退回第k个版本,当然,如果我们有root数组的话,我们可以这么写:

    rootfa[ver]=rootfa[x];
    rootdep[ver]=rootdep[x];
    

    ver指向当前版本,我们可以很简单地直接把x版本的root值复制过来,这样他们就都指向同一颗主席树,也就相当于把整个版本都复制过来。

    然后就是merge函数,merge函数其实也不难,就照着普通按秩合并的代码一通乱改就好了:

    void merge(int ver,int x,int y) {
    	x=find(ver-1,x);
    	y=find(ver-1,y);
    	if(x==y) {
    		rootfa[ver]=rootfa[ver-1];
    		rootdep[ver]=rootdep[ver-1];
    	} else {
    		int depx=query(1,n,rootdep[ver-1],x);
    		int depy=query(1,n,rootdep[ver-1],y);
    		if(depx<depy) {
    			modify(1,n,rootfa[ver-1],rootfa[ver],x,y);
    			rootdep[ver]=rootdep[ver-1];
    		} else if(depx>depy) {
    			modify(1,n,rootfa[ver-1],rootfa[ver],y,x);
    			rootdep[ver]=rootdep[ver-1];
    		} else {
    			modify(1,n,rootfa[ver-1],rootfa[ver],x,y);
    			modify(1,n,rootdep[ver-1],rootdep[ver],y,depy+1);
    		}
    	}
    }
    

    关于为什么ver要-1的问题,因为个人代码的写法(其他很多dalao的板子里面没这个问题)问题,当我们的程序运行到merge之前(可以理解为你在merge那里打了一个断点,然后程序在这个断电处暂停时的状态),此时的ver虽然是指向着一个新的版本,但是这个版本内什么都没有,所以我们的merge函数需要的是root[ver-1]指向的版本内的值,所以要注意在merge函数里面每次modify和query时其所指向的历史版本都应该是ver-1(反正不管我们是查询还是退回最后都是在两个root数组后面修改)。然后就是要注意每次如果对dep数组没影响的时候就要把ver-1版本的dep数组给同步到ver版本里面去,反之,如果有修改的话就不要同步了。

    同理,我们的find函数本来也是要ver-1的,但是因为query并没有涉及到任何数组的更改,所以我们直接在Main函数内的find之前:(后面的AC代码里面有写到)

    rootfa[ver]=rootfa[ver-1];
    rootdep[ver]=rootdep[ver-1];
    

    可能现在你有一个疑惑,既然find可以这么搞,那么为什么merge不能也这么搞呢?

    是这样的,我们回到可持久化数组的模板里面可以发现,在第一行里面就已经是一个给now+1的语句,并且这里的now传的还是引用。如果我们在merge之前写上这两句话,我们最终更改的就是rootfa[ver+2],这肯定是不对的,所以我们要手动给ver-1而不是直接复制整个root

    下面给出模板题的AC代码:

    #include <bits/stdc++.h>
    using namespace std;
    const int maxn=1e6+10;
    struct node {
    	int l,r,sum;
    } t[maxn*40*2];
    int n,m,tot,cnt,rootfa[maxn],rootdep[maxn];
    void build(int l,int r,int &now) {
    	now=++cnt;
    	if(l==r) {
    		t[now].sum=++tot;
    		return;
    	}
    	int mid=(l+r)/2;
    	build(l,mid,t[now].l);
    	build(mid+1,r,t[now].r);
    }
    void modify(int l,int r,int ver,int &now,int pos,int num) {
    	t[now=++cnt]=t[ver];
    	if(l==r) {
    		t[now].sum=num;
    		return;
    	}
    	int mid=(l+r)/2;
    	if(pos<=mid) modify(l,mid,t[ver].l,t[now].l,pos,num);
    	else modify(mid+1,r,t[ver].r,t[now].r,pos,num);
    }
    int query(int l,int r,int now,int pos) {
    	if(l==r) return t[now].sum;
    	int mid=(l+r)/2;
    	if(pos<=mid) return query(l,mid,t[now].l,pos);
    	else return query(mid+1,r,t[now].r,pos);
    }
    int find(int ver,int x) {
    	int fx=query(1,n,rootfa[ver],x);
    	return fx==x?x:find(ver,fx);
    }
    void merge(int ver,int x,int y) {
    	x=find(ver-1,x);
    	y=find(ver-1,y);
    	if(x==y) {
    		rootfa[ver]=rootfa[ver-1];
    		rootdep[ver]=rootdep[ver-1];
    	} else {
    		int depx=query(1,n,rootdep[ver-1],x);
    		int depy=query(1,n,rootdep[ver-1],y);
    		if(depx<depy) {
    			modify(1,n,rootfa[ver-1],rootfa[ver],x,y);
    			rootdep[ver]=rootdep[ver-1];
    		} else if(depx>depy) {
    			modify(1,n,rootfa[ver-1],rootfa[ver],y,x);
    			rootdep[ver]=rootdep[ver-1];
    		} else {
    			modify(1,n,rootfa[ver-1],rootfa[ver],x,y);
    			modify(1,n,rootdep[ver-1],rootdep[ver],y,depy+1);
    		}
    	}
    }
    int main(void) {
    	scanf("%d %d",&n,&m);
    	build(1,n,rootfa[0]);
    	for(int ver=1; ver<=m; ver++) {
    		int opt,x,y;
    		scanf("%d",&opt);
    		if(opt==1) {
    			scanf("%d %d",&x,&y);
    			merge(ver,x,y);
    		} else if(opt==2) {
    			scanf("%d",&x);
    			rootfa[ver]=rootfa[x];
    			rootdep[ver]=rootdep[x];
    		} else {
    			scanf("%d %d",&x,&y);
    			rootfa[ver]=rootfa[ver-1];
    			rootdep[ver]=rootdep[ver-1];
    			int fx=find(ver,x),fy=find(ver,y);
    			printf("%d
    ",fx==fy?1:0);
    		}
    	}
    }
    

    BTW,如果要维护集合的属性的化(比如说是集合大小之类的?),就要用到可持久化带权并查集

    其实也非常简单,就是在可持久化数组里面新开一个变量去维护

  • 相关阅读:
    .net环境中引用js文件乱码解决办法(转)
    js中frame对象几种访问方法
    js中建立像泛型、哈希表那样的对象
    VB编程操作AutoCAD长度型尺寸标注
    VB编程设置AutoCAD文字样式
    VB编程操作AutoCAD角度型尺寸标注
    VB编程操作AutoCAD单行文字
    VB编程操作AutoCAD块对象
    VB编程控制AutoCAD颜色属性
    VB编程操作AutoCAD坐标型尺寸标注
  • 原文地址:https://www.cnblogs.com/jrdxy/p/12584745.html
Copyright © 2011-2022 走看看