冰茶姬(一种饮料?)

并查集(正经

并查集是一种支持动态 link 的,能够快速判断两元素是否处于同一集合的数据结构。

(如果需要 cut,请出门右转 LCT

那么其基本操作就包括:

  1. 连边。在两个点之间连一条边,使得两个点所在的两个集合合并为一个集合。

  2. 查询。查询两个点是否处在同一个集合中。

这也是普通并查集的所有操作了。

题单

暴力并查集

模板

核心

当连边的时候,如果两个点本来就在一个集合里,是不需要再连边的。因此整个并查集的形态便是森林。

一般而言,树都是有根的,那么确定两个点分别属于哪个集合的时候,就可以找到并判断它们的根是否相等。

而连边的时候,也仅需要将两个根连接起来,因此找根是并查集的核心操作。

既然要找根,那么也就需要知道点与点之间的父(子)关系(确定父关系足矣)。

初始的时候,每个点各自为营,那么其父亲就是他们自己(我当我的爹)。

for(int i=1;i<=n;i++)fa[i]=i;

每次操作的时候,不管是连边还是查询,都需要先找到两个点所在树的根。

一个很朴素的想法就是,既然知道父关系,那么就根据父关系一路往上跳。

根据初始化,直到当前节点的父亲为自己,就是找到根了。

代码如下:

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

找到根之后,这两个操作也就很容易实现了:

for(int i=1;i<=m;i++)
{
	op=re();u=re();v=re();
	int r1=find(u),r2=find(v);
	if(op==1)fa[r1]=fa[r2];
	else puts(r1==r2?"Y":"N");
}

时间复杂度

一般而言,时间复杂度都会有平均、最优、最差等几种。

最优一般是没有讨论意义的,因为总有好(du)人(liu)卡极限数据,那么此处就讨论一下极限数据时的最差情况。

在极限数据下,会出现这种情况:

每次将当前最大的树的根连在一个单节点树上,最终这棵树的深度则为 \(n\),此时最坏情况下的单次查询的复杂度为 \(O(n)\),总复杂度为 \(O(qn)\)

这时候也就需要有一个优化。

路径压缩

由上可以发现,其实我们只期望知道根而不关心路径上通过了哪些点。

那么是否可以每次将我们所经过的点的父亲都更新成根呢?

当然可以,只需要将我们的 find 改成这样:

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

相当于是一次记忆化搜索,核心就在于 fa[s]=find(fa[s]),在递归的过程中,不断更新经过的点的父亲,以达到路径压缩的目的,下次找 s 点的时候便能直接指向其根。

时间复杂度

这个优化计算时间复杂度是通过势能分析得到的均摊复杂度,证明在这(反正我是看不懂)。

按秩合并 / 启发式合并

除了路径压缩,还有一个很简单的想法,每次将小的合并到大的身上,这样就可以避免出现深度过深的情况了。

按秩合并和启发式合并是两种计算大小的方法,一种是计算树的大小,另一种是计算树的深度。

由于二者十分相似,一般不会太过细致的区分,不同的文章写的也不一样。

按大小(秩)的话比较简单,直接比较两个树的 siz 大小,将小的合并到大的上即可。

int find(int x){return fa[x]^x?(find(fa[x])):x;}
void merge(int x,int y)
{
	x=find(x),y=find(y);
	if(siz[x]>siz[y])swap(x,y);
	fa[x]=y;siz[y]+=siz[x];
}

但是实测按大小合并的复杂度并不优秀。

而按深度合并(启发式)则稍微复杂一些,因为如果两个树的深度不相等的话,小树合并到大树是不会影响大树的深度的,而两树深度相同的时候,合并才会使整体的深度 +1。

int find(int x){return (fa[x]^x)?(find(fa[x])):x;}
void merge(int x,int y)
{
	x=find(x),y=find(y);
	if(dep[x]>dep[y])
		swap(x,y);
	fa[x]=y;
	if(dep[x]==dep[y])
		dep[y]++;
}

这两种方法的 find 则不需要使用路径压缩。

当然,这两种优化方式是可以一起使用的,就是启发式合并的时候也使用路径压缩。但是对于大多数数据而言,单独使用两种优化也是足够优秀的,所以同时使用两种优化并不比单独使用其中一种优秀很多。

完整代码自己打(懒

一道好题

需要用到离散化

基础应用

并查集的应用有最小生成树,虽然一些其他算法也有用到并查集(比如某些 DP左偏树),但是 kruskal 可以说是其代表了。

边带权

例题

思路:

显然这是一道并查集判连通的题目,但是不同之处在于,这道题不仅需要判断两个点是否处于同一连通块,查询时需要知道两个点之间的距离。

如果用一个二维数组去储存两点之间的距离的话,需要的空间是 \(O(n^2)\) 的,显然不允许。

思考并查集有一个什么特点。没错,每个点都能很容易的找到自己的根节点。那么如果我们储存每个点到其根节点的距离,如果两个点在同一个集合里时,就可以通过到根节点的距离之差来代表两个点之间的距离,空间复杂度 \(O(n)\),可以接受。

这种需要考虑点之间距离的并查集,就叫做边带权。

dis 来代表当前节点到根节点的距离,siz 代表当前并查集的大小。

找根时,除了需要更新 fa,还需要更新 dis

int find(int x)
{
	if(fa[x]==x)return x;
	int f=find(fa[x]);
	dis[x]+=dis[fa[x]];
	return fa[x]=f;
}

简单分析可以发现,多次 find 的时候,每个节点的 dis 并不会多次更新。

合并的时候,同样需要更新 dis,不过只需要更新根节点。同时更新根节点的 siz

void merge(int x,int y)
{
	int r1=find(x),r2=find(y);
	fa[r2]=r1;
	dis[r2]=siz[r1];
	siz[r1]+=siz[r2];
}

查询操作则最为简单。

完整代码:

const int inf=3e4+7;
int n,fa[inf];
int siz[inf],dis[inf];
int find(int x)
{
	if(fa[x]==x)return x;
	int f=find(fa[x]);
	dis[x]+=dis[fa[x]];
	return fa[x]=f;
}
void merge(int x,int y)
{
	int r1=find(x),r2=find(y);
	fa[r2]=r1;
	dis[r2]=siz[r1];
	siz[r1]+=siz[r2];
}
int main()
{
	n=re();
	for(int i=1;i<=inf;i++)
		fa[i]=i,siz[i]=1;
	for(int i=1;i<=n;i++)
	{
		char op[10]="";scanf("%s",op);
		int x=re(),y=re();
		if(op[0]=='M')merge(x,y);
		else
		{
			int r1=find(x),r2=find(y);
			if(r1^r2)puts("-1");
			else wr(abs(dis[x]-dis[y])-1),putchar('\n');
		}
	}
	return 0;
}

拓展域

模板

拓展域一般用于解决一个点有多种性质的问题,如上。

一个人可以分为两个域:朋友域和敌人域。

根据朋友的朋友是朋友,可以得到朋友域处于同一个集合代表这些人互为是朋友。

而根据敌人的敌人是朋友,如果小甲和小乙互为敌人,小乙和小丙互为敌人,那么小甲和小丙应该是朋友,但是小甲和小丙不能直接通过朋友域连接,而是应该通过小乙的敌人域进行连接。

所以敌人域不会直接相连,它仅仅起到对朋友域进行间接连接的作用。

理论知识比较简单,而代码实现时,一般用 i,i+n 等来代表第 \(i\) 个人的不同域。

完整代码:

const int inf=2e3+7;
int n,m,ans,fa[inf];
bool vis[inf];
int find(int x){return (fa[x]^x)?(fa[x]=find(fa[x])):(x);}
int main()
{
	n=re();m=re();
	for(int i=1;i<=n*2;i++)fa[i]=i;
	for(int i=1;i<=m;i++)
	{
		char op[2]="";scanf("%s",op);
		int u=re(),v=re();
		if(op[0]=='F')
			fa[find(u)]=find(v);
		if(op[0]=='E')
		{
			fa[find(u)]=find(v+n);	
			fa[find(v)]=find(u+n);
		}
	}
	for(int i=1;i<=n;i++)
	{
		int ls=find(i);
		if(vis[ls])continue;
		vis[ls]=1;ans++;
	}
	wr(ans);
	return 0;
}

(这个图可以点的啊喂)

可持久化

其实仅仅是用可持久化数组优化了并查集的 fadep 数组,使得并查集变得可持久化。

posted @ 2023-11-20 19:44  Zvelig1205  阅读(58)  评论(0编辑  收藏  举报