2019.9.17 初级数据结构——并查集及其应用

一、并查集基础

(一)引入

我们先来看一个问题。

某学校有N个学生,形成M个俱乐部。每个俱乐部里的学生有着一定相似的兴趣爱好,形成一个朋友圈。一个学生可以同时属于若干个不同的俱乐部。根据“我的朋友的朋友也是我的朋友”这个推论可以得出,如果A和B是朋友,且B和C是朋友,则A和C也是朋友。请编写程序计算最大朋友圈中有多少人。

输入格式:
输入的第一行包含两个正整数N(≤30000)和M(≤1000),分别代表学校的学生总数和俱乐部的个数。后面的M行每行按以下格式给出1个俱乐部的信息,其中学生从1~N编号:

第i个俱乐部的人数Mi(空格)学生1(空格)学生2 … 学生Mi

输出格式:
输出给出一个整数,表示在最大朋友圈中有多少人。

输入样例:
7 4
3 1 2 3
2 1 4
3 5 6 7
1 6
输出样例:
4

我们把这个题抽象化:

现在有n个数,它们按照一定规律合并,形成一些集合,求最大集合的元素个数。

这个合并规律如下:

(1)同一个俱乐部的所有人属于同一个集合;

(2)若两个俱乐部包含同一个人,则这两个俱乐部属于同一个集合。

按照正常做法,我们对于每一个人按照上述规律扫描剩余的人,运行复杂度O(n^很大)。

这个方法显然过不了。

我们重新分析这个题目,主要难点有两个:

第一,对于第一条,同一个俱乐部的所有人如何存储它们所在同一个集合内。

这个很好解决,我们再开一个book数组,对于每一个人i,假设它在cnt集合内,我们标记book[i]=cnt。

如果这样标记,对于第二个合并规律,我们扫描另一个俱乐部的所有人,我们把每一个book[i]都置成要合并到的俱乐部,总复杂度O(nm)。

但这样还是过不了,所以我们考虑把第二个合并步骤的复杂度优化到O(1),也就是说我们只需要修改一个数就可以修改整个集合。

对于这样的修改方式,我们可以想到一种数据结构——

利用树存储每个集合有两个好处:

第一,每棵树只有一个树根(也就是查找树根就可以查找整棵树)

第二,两棵树合并只需要把一棵树的树根接到另一棵上(所以修改的复杂度是O(1))。

大概就是上面这么合并

也就是说我们可以用树优化查找和合并集合的过程,其本质就是对树之间的处理。

又因为,这一类题的根本步骤就是查找合并,所以我们把这样的数据结构叫做并查集


(二)并查集的基本操作

刚才我们已经知道,并查集中合并两个元素实际上是合并他们所在树的祖先。

为了查询这两个点是否在一棵树上,我们首先要找到他们两个点的祖先分别是谁,并判断他们的祖先是否是同一个点即可。

但是因为树会退化成一条链,所以我们在查找祖先时要把下图的第一个树变成第二个树,这是后话。

第二个图的意思就是所有的点都连到根节点上。

我们暂且不提怎么查找一个点的祖先,在我们查找两个点祖先之后,判断他们的祖先是不是同一个点,如果不是同一个点合并即可。

所以我们用fa[x]表示x的父亲,则代码如下:

void u(int a,int b)
{
	int c=f(a),d=f(b);
	if(fa[c]!=fa[d])
	{
		fa[min(c,d)]=fa[max(c,d)];
		return;
	}
}

一定要注意的是,绝对不能让fa[min(a,b]直接等于fa[max(a,b)],因为这样只合并了两个点,而不是其所在的树。

同时,注意合并时要按照一定的大小顺序,所以这里是按小往大合的。

所以这是合并两个点的方法。

关于查找祖先的方法,有一个非常绝妙的方法:

int f(int o)
{
	if(fa[o]==o)return o;
	return fa[o]=f(fa[o]);
}

第一行非常容易理解,如果o的祖先时自己则它自己就是这棵树的祖先。

第二行我们把它分解成两部分:

(1)f(fa[o])找到fa[o]的祖先。

(2)return fa[o]=f(fa[o]) 将o的父亲置为其父亲的祖先

也就是说 我们成功把一棵近似于一条链的树 弄成了一棵最多只有三层的树。

这个过程换句话说是这样:

(1)递归地找点o每一级祖先o',直到找到它真正的祖先o1;

(2)回溯,对于每一个o',将fa[o]置为o1.

也就是说 通过这个步骤,这棵树的每一个点都直接连接到了祖先上

所以回到这章开始的部分,对于那两棵树之间的变化,有一种方法就是上面说的更改每个点与祖先的连接方式,这种方式叫作路径压缩

其实还有一个稍微好实现一点的方法,对于每个树根,我们记录它所在的树的高度,把这个高度叫作这棵树的

对于每个合并操作(不是路径压缩时的查找操作),我们把秩小的树合到秩大的树上,这样可以保证合出来的新树的秩不大于原来任何一棵树

若两棵树的秩一样,则新树的秩是原树的秩+1.

这种方法也叫按秩合并

最后记得一开始初始化,令fa[x]=x。

上个板子:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int fa[100050];
int f(int o)
{
	if(fa[o]==o)return o;
	return fa[o]=f(fa[o]);
}
void u(int a,int b)
{
	int c=f(a),d=f(b);
	if(c!=d)fa[min(c,d)]=fa[max(c,d)];
	return;
}
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)fa[i]=i;
	for(int i=1;i<=m;i++)
	{
		int x,y,opt;
		scanf("%d%d%d",&x,&y,&opt);
		if(!opt)u(x,y);
		else
		{
			if(f(x)!=f(y))puts("NO");
			else puts("YES");
		}
	}
	return 0;
}

  高级用法待填坑。

/*====年轻人,瞎搞是出不了省一的,这就是现实====*/
原文地址:https://www.cnblogs.com/qxds/p/11536627.html