数据结构--并查集

并查集

并查集是一种树型数据结构,用于解决不相交集合的合并和查询问题。
具体的例子:假设 “朋友” 这个关系满足如下性质:

  • 如果 a 和 b 是朋友,那么b 和 a 是朋友。
  • 如果 a 和 b 是朋友,b 和 c 是朋友,那么a 和 c 是朋友。
  • 一个人和他的所有朋友称为一个 “集体”。

现在给出m对朋友关系 (u, v) 表示u 和 v 是朋友,问:

  • 问题1:给定两个人a 和 b 求他们是否是朋友。
  • 问题2:一共有多少个 “集体”。

可以看出来这个问题需要维护一种传递关系,那么我们可不可以用Floyed算法求他的传递闭包呢?理论上来说Floyed算法可以解决,用link[i][j]表示结点间的连通关系,则执行完Floyed以后,link[i][j] = 1,则 i 和 j 是朋友,反之不是。但是,Floyed回答问题2 就比较麻烦了,而且更重要的一点,Floyed有很多算法无可匹敌的时间复杂度 O(n^3),因此Floyed算法解决此问题是很低效的。更更更重要的一点,Floyed只能离线,这个懂的自然懂,不懂也没关系,知道不能用Floyed解决这个问题就可以了。

并查集的基本思想

  1. 并查集维护集合关系,初始时,为每一个元素建立一个集合,这个集合只有这个元素自己。
  2. 我们需要随时根据给出的,某两个元素之间的关系,将两个元素所在的集合合并(a 和 b是朋友,那么a的朋友也是b的朋友,b的朋友也是a的朋友,相当于两个集合合并了)
  3. 能随时查询两个元素当前是否在一个集合里。

并查集的操作

并查集是树形数据结构,我们将每一个集合视为一棵树。
既然是树,必然有一个根节点,这个根节点,我们定义为这个集合的代表,此后我们都用集合的代表来代替一个集合。
也就是说:同一个代表,同一个集合
我们定义 father[] 数组,father[i] 表示 i 结点的父节点
初始时所有元素的父节点都是自己,代表也是自己,因为此时一个集合只有一个元素,就是他自己。
数据结构--并查集_第1张图片
这时我们可以对集合进行合并操作了,假设现在合并结点1 和 结点2所在的集合,两个集合变成一个,两个集合的代表也就只能留下一个,留下哪个呢?------无所谓。
假设我们把2 并入 1 就直接把father[2] = 1,这样1 和 2,就在一起了。
数据结构--并查集_第2张图片
2 的父节点是 1 ,{1,2} 这个集合的代表是 1 ,它是根节点。
然后,我们把2 和 3 所在的集合合并,按照前面的说法,直接令father[2] = 3或者 father[3] = 2 吗?
那就错了。
如果直接令father[2] = 3,相当于把 1 号结点扔到一边了,此后2 和 3 的集合里就没有1 结点了。father[3] = 2 虽然看上去没问题,但其实和前面是一样的问题,如果 3 结点有父节点,也相当于都被忽略了。

前面说过了,代表集合的树的根节点叫这个集合的代表,所有操作都是针对这个代表的。也就是说,合并两个集合,首先要找到两个集合的代表,然后将其中一个接到另一个上面。怎么找代表呢?一层一层找,知道某个结点的父节点是它自己,这个结点就是根节点,也就是代表。
也就是说,我们想合并2 和 3 所在结点,需要找到两个代表,分别是1 和 3,令father[1] = 3 或 father[3] = 1 都可以。
(图片)
合并操作结束了,怎么判断两个元素是否在一个集合里呢?刚才说了,同一个集合,同一个代表,两个元素同处于一个集合则两个元素的代表一定相同。
最后要求一共有多少集合,一个集合是一棵树,有多少个根节点就有多少棵树,也就是说,检查所有 father[],有多少元素根节点等于自己,就有多少集合
代码:

int find(int x)       //找到x所在集合的代表(根节点)
{
	if(father[x]!=x)
	    return find(father[x]);
	else
	    return x;
}
void combine(int x, int y)    //合并x 和 y 所在集合
{
	int r1 = find(x);       //一定先找到两个元素的代表
	int r2 = find(y);
	if(r1!=r2)
	    father[r2] = r1;     // 把一个代表接到另一个上
}
int query(int x, int y)      //查询x 和 y 是否在一个集合里
{
    return (find(x)==find(y)) ? 1 : 0;
}
//别忘了最开始初始化father[i] = i

并查集的路径压缩

并查集有一个和二叉树一样的问题,他的时间复杂度很容易退化。比如要查找的一条链老长,查找的复杂度会变成 O(n)。
数据结构--并查集_第3张图片
如何避免这样呢?并查集有一个路径压缩的操作,即每次查找一个元素的代表时,将查找路径上的所有结点转接到代表上,具体见代码:

int find(int x)
{
	if(father[x]!=x)
	    father[x] = find(father[x]);     //把x直接接在x所在集合的代表上
	return father[x];
}

这样执行完find(5) 后,整个图的状态是这样的,它保证了并查集的始终高效。
数据结构--并查集_第4张图片

并查集例题

并查集模板:洛谷P3367 【模板】并查集.
并查集+映射:洛谷P2814 家谱.
扩展域并查集:洛谷P1892 [BOI2003]团伙.(题目描述有一点小问题)

你可能感兴趣的:(数据结构)