浅说并查集

浅说并查集

并查集是一种树形的数据结构,顾名思义,它用于处理一些集合的合并查询问题。

初始化

并查集的基础是一个数组 \(fa\)\(fa_x\) 代表 \(x\) 的“上级”。初始每个点的“父亲”都是它自己,即 \(fa_i=i\)

之后并查集的题目会给出一些关系,将一些点连接起来,所以这些点的父亲可能会发生改变:

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

合并及查询

并查集的核心。

查询

在并查集的题目中,我们会知道所有点的“父亲”是谁。那我们怎么知道这个点最老的祖先是谁呢?

对于这个点,只需要一层一层地往上查询,就能知道这个点最老的祖先了。

我们可以用递归思想求出这个点最老的祖先:

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

你也可以使用三目运算符缩小代码复杂度:

int find(int x){
	return fa[x]==x?x:find(fa[x]);
}

合并

查询都有了,那合并总得弄一个吧。不合并怎么知道这个点的祖先到底是谁。

如下图,直接把一棵树合并到另一棵树里(合并树根就行了):

都看到了,要把两个数合并到一个集合里,得把这两个集合的根节点中的一个连到另一个上:

void connent(int x,int y){
	int fx=find(x),fy=find(y);
	if(fx!=fy) fa[fx]=fy;
}

优化

查询优化:路径压缩

众所周知,我们在查询的时候,如果树的高度太高,查询的时候这个时间复杂度就很难受。得想个优化办法!

想一下,如果把每一个点的“父亲”直接变成它最老的祖先,不就行了吗?

这个操作只需在查询的同时更改每个点的上级就行了:

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

或三目运算符:

int find(int x){
	return fa[x]==x?x:fa[x]=find(fa[x]);
}

这个方法可以超级有效地优化时间复杂度,但是会破坏树的结构,在一些情况下不能使用。

合并优化:按秩合并

这个优化方法可以从根本上解决树的高度的问题。

很简单,只要开一个 \(h\) 数组记录每一棵树的高度,再把高度低的树合并到高度高的树上:

void connent(int x,int y){
	int fx=find(x),fy=find(y);
	if(fx!=fy){
		if(h[fx]<h[fy]) fa[fx]=fy;
		else if(h[fx]>h[fy]) fa[fy]=fx;
		else fa[fx]=fy,h[fy]++;
	}
}

优化总结

路径压缩和按秩合并同时使用,查询操作的时间复杂度有时甚至可以降到 \(O(1)\)

路径压缩的优化程度直接把我 \(1.2\texttt s\) 的数据点直接降到 \(700\texttt{ms}\) 了(洛谷题库)。

按秩合并没有路径压缩的优化效果强。所以我喜欢只用路径压缩。其实是我懒得打按秩合并

最小生成树:\(\textrm{Kruskal}\)

从边入手的求最短路的算法。

基于贪心;从小到大加边。加边、合并、查找的操作都基于并查集。

给边的权值排序,再从小到大(最大生成树反过来)扫一遍,计数到 \(n-1\) 时停止算法,输出答案。扫的过程中,如果两个点的祖先不一样,及两个点尚未联通,我们才能添加这两个点之间的路径:

int kruskal(){
	sort(a+1,a+m+1,cmp);
	int sum=0,egs=0;
	for(int i=1;i<=m;i++){
		int fs=find(a[i].s),ft=find(a[i].t);
		if(fs!=ft){
			sum+=a[i].v,egs++;
			fa[fs]=ft;
			if(egs==n-1) return sum;
		}
	}
	return -1;
}
posted @ 2023-08-25 10:49  Clay_L  阅读(34)  评论(2)    收藏  举报