举个栗子
有一个庞大的传销组织,成员之间有着上线和下线的关系(上线可以理解为上级)。如下是这个组织的关系图
(黑色姓代表一级上线)
可见该组织有5个一级上线(赵、钱、孙、张、孔),分别下辖若干级下线。张某只有一个下线,而孔某一个下线也没发展到。
现在该组织面临一个难题。组织虽然人数众多,可每个成员只认识自己的直接上级,除此之外谁也不认识。这给他们之间的信息传递带来了极大的困难。因为每个一级上线以及其下辖的下线都是一个对外独立的团队,只有属于同一团队的成员之间才能交换信息,否则就会造成机密泄露。
例如秦某和朱某相遇了,他们想在交换信息前确认对方是不是自己人,于是他们分别给自己的上级打电话(沈某和韩某)。可问题是沈某和韩某也互不认识,于是又分别给自己的上级打电话。庆幸的是,他们两的上级都是钱某。一通信息反馈回来,秦某和朱某终于互相确认对方是自己人,两人喜极而泣,紧紧拥抱在一起,然后交换了信息。
这样的案例每天在组织内部数不胜数地发生。这样极大降低了信息传递的效率。该组织的BOSS决定精简组织内部的人事架构以便于管理。具体怎么做呢?就是将原来的多级下线精简为一级下线。
以上图中的钱氏团队为例,精简后的效果如下:
如此一来,当传销组织中的任意两个下线相遇,他们只需要打一次电话就能确认对方是否是自己人。这样就极大提高了效率。
但组织的BOSS不知道怎样做才能快速完成这项工作,于是他把这项工作交给了精于编程的你……
同样的例子还有
实例讲解
我们先进行一个设定:
对于一个非空的集合A,规定:若一个非空集合B与集合A存在相同的元素,那么便将集合B并入集合A。这个操作实际上就是将交集非空的两个集合进行合并。
以钱某团队为例,
规定一个上线和其直接管理的一个下线构成集合B(n),集合A代表钱某团体(刚开始时只有{钱、沈})。
- 集合B(1)({钱,韩})与集合A有相同元素 钱,于是将B(1)并入集合A。此时集合A为{钱、韩、沈};
- 集合B(2)({钱,杨})与集合A有相同元素 钱,于是将B(2)并入集合A。此时集合A为{钱、韩、沈、杨};
- 集合B(3)({沈,朱})与集合A有相同元素 沈,于是将B(3)并入集合A。此时集合A为{钱、韩、沈、杨、朱};
- 集合B(4)({韩、秦})与集合A有相同元素 韩,于是将B(4)并入集合A。此时集合A为{钱、韩、沈、杨、朱、秦};
- 集合B(5)({秦、许})与集合A有相同元素 秦,于是将B(5)并入集合A。此时集合A为{钱、韩、沈、杨、朱、秦、许};
原先5个分散的小团队被合并成了一个大团队。
然后,以钱某为团队上线,其他人为他的直接下线,构建新的从属关系:
如此一来,任意两个组织成员再想确认是不是就方便多啦。
概念解析
以下是百度百科对并查集的解释:
我的解释:
并查集是利用树形结构实现的。
每个元素对应一个节点,每个组对应一棵树。在并查集中,哪个节点是哪个节点的父亲(父结点)以及树的形状等信息无需多加关注,整体组成一个树形结构才是重要的。
此外,我们把类似上图中1、5、4那样的结点叫做根结点(可以理解为组长、团队领导之类的)。显然,5是自身的根结点。
实现步骤
初始化
给出n个结点来表示n个元素(此时没有边);
给出若干组结点间关系,合并相连的两个组
查询
为了查询两个结点是否属于同一组,我们需要沿着树向上走,来查询包含这个元素的树的根结点是谁。如果两个结点走到了同一个根结点,那么就可以知道他们属于同一组。
例如在下图中,
6、7的根结点是1,所以6、7属于同一集合。
路径压缩(优化并查集结构,减小时间复杂度)
对于每个结点 i ,一旦向上走到了一次根结点,就把结点 i 到父结点的边改为直接连向根结点。
例如,
代码示例
#include <stdio.h>
#include <stdlib.h>
//并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)
//的合并及查询问题.常常在使用中以森林来表示
int filiation[100];//filiation意为父子关系,此数组用于存储两结点的上下级关系
int search_root_node(int random_node)//查找根结点;random_node任意结点
{
int root_node;//root_node用于储存最后找到的根结点
int intermediary=random_node;//intermediary意为中介,这里用作中介变量
//对于根结点root_node,有filiation[root_node]==root_node;
//非根结点元素不满足此关系
while(filiation[intermediary]!=intermediary)//若当前元素的父结点不是根结点
{
intermediary=filiation[intermediary];//继续查看父结点的父结点是不是根结点
}
//退出循环后intermediary即为根结点
root_node=intermediary;//将找到的根结点赋给root_node
return root_node;
}
int merger(int random_node,int root_node)//merge意为合并,即路径压缩
{//将查找路径中所有属于根结点但父结点不是根结点的结点的父结点换成根结点
int j;
while(random_node!=root_node)
{
j=filiation[random_node];
filiation[random_node]=root_node;
random_node=j;
}
return 0;
}
int main()
{
int n,m,i,a,b;//n为结点总数,m为路径数量
int a_root,b_root;
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++)//刚开始所有结点的父结点都是自己
{
filiation[i]=i;
}
for(i=1;i<=m;i++)
{
scanf("%d%d",&a,&b);//输入若干组两结点间的关系
//寻找结点a、b的根结点
a_root=search_root_node(a);
b_root=search_root_node(b);
//路径压缩
merger(a,a_root);
merger(b,b_root);
if(a_root!=b_root)//说明a,b两结点应属于同一个根结点但实际上没有
{
filiation[a_root]=b_root;//连接a_root与b_root
}
}
return 0;
}