数据结构——可持久化并查集

前置知识

可持久化数组

简介

先来一道模板题:

可持久化并查集

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

  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\n",fx==fy?1:0);
		}
	}
}

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

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

posted @ 2020-03-29 03:02  菜鸡mk  阅读(390)  评论(0编辑  收藏  举报