关于并查集,我是通过点击量最多的那几个博客学习的,他们所举的例子也比较通俗易懂。先贴一个我学习时参考的博客链接:
【算法与数据结构】—— 并查集
从里面学到了“并”“查”,所谓并就是将两个不相交的集合合并,查就是在集合里查询问题。
下面是并查集的并和查的未优化版本代码:
int find(int x)//查找x的节点,查
{
while(pre[x] != x)
x = pre[x];
return x;
}
因为数组中下标位的值存储的是该下标的所属集合,如pre[i]=3 就表示i是属于3这个集合的,3是i的父节点,而pre[3]=1,就说明3又是属于1的,1是3的父节点。如果pre[1]=1 就说明1是这个树的根节点。就依据这个规律可以找到一个树的根节点。
void join(int x,int y)//合并两个集合
{
int fx=find(x), fy=find(y);
if(fx != fy)
pre[fx]=fy;
}
并需要知道两个集合的根节点,直接将一个根节点的值赋给另一个,就完成了合并,如pre[3]=3,pre[5]=5。将前者赋给后者就变成了pre[5]=3;通过查操作时查5的父节点时就查到了3这个根节点。
并和查是简单的并查集的两板斧,这个模板通用。另外就是路径压缩的问题,因为并查集树形结构他会出现一个特别深的树和一个特别短的树合并,在合并时找根节点就会变得非常麻烦,而且合并后新的树查找起来效率也不高。这时就引入了压缩路径。如何压缩路径呢?分别在并和查上面压缩。
在查上面我们可以在找父节点的时候直接将父节点的值赋值给当前的下标所代表的值
int find(int x) {//递归实现
if (pre[x] == x) // x 结点就是根结点
{
return x;
}
return pre[x] = find(pre[x]);// return find(pre[x]); // 返回父结点的根结点 // 返回父结点的根结点,并令当前结点父结点直接为根结点
}
在并时,我们可以发现树的深度低的并到高的上面会尽量的减少操作数量,因为树的深度越深,合成的新树的深度就会+1,所以将短的并到长的上面可以尽可能减少复杂度。我们定义一个存深度的数组。
void join(int x,int y){//优化
int fx=find(x);
int fy=find(y);
if(height[fx]!=height[fy]){
if(height[fx]>height[fy]){
pre[fx]=fy;
}
if(height[fx]<height[fy]){
pre[fy]=fx;
}
}
else{
height[fx]=height[fx]+1;
pre[fy]=fx;
}
}
最后放一个题
当今世界有许多不同的宗教,很难跟踪它们。你有兴趣找出你们大学里有多少不同的宗教学生相信。
你知道你的大学里有 n 名学生 (0 + n = 50000) 。你问每个学生他们的宗教信仰是不可行的。此外,许多学生不愿意表达他们的信仰。避免这些问题的一种方法是问m(0 lt;\m <\n(n-1)/2)对学生,问他们是否相信同一种宗教(例如,他们可能知道他们是否都参加同一个教会)。从这些数据中,你可能不知道每个人相信什么,但你可以了解校园里有多少不同的宗教可以代表。您可能认为每个学生最多只订阅一种宗教。
输入
输入由多个案例组成。每个案例都以一行开头,该行指定整数 n 和 m。下一个m线各由两个整数i和j组成,指定学生i和j相信同一宗教。学生编号为1到n。输入的末尾由 n = m = 0 的行指定。
输出
对于每个测试案例,在一行上打印案例编号(从 1 开始),然后是大学学生所信仰的不同宗教的最大数量。
Sample Input
10 9
1 2
1 3
1 4
1 5
1 6
1 7
1 8
1 9
1 10
10 4
2 3
4 5
4 8
5 8
0 0
Sample Output
Case 1: 1
Case 2: 7
附上代码
#include <iostream>
#include <bits/stdc++.h>
#include <stack>
#include <queue>
#include <algorithm>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <string.h>
using namespace std;
int pre[100005],height[100005];
//int find(int x)//查找x的节点
//{
// while(pre[x] != x)
// x = pre[x];
// return x;
//}
int find(int x) {//同上,递归实现
if (pre[x] == x) // x 结点就是根结点
{
return x;
}
return pre[x] = find(pre[x]);// return find(pre[x]); // 返回父结点的根结点 // 返回父结点的根结点,并令当前结点父结点直接为根结点
}
//void join(int x,int y)//合并两个集合
//{
// int fx=find(x), fy=find(y);
// if(fx != fy)
// pre[fx]=fy;
//}
void join(int x,int y){//优化
int fx=find(x);
int fy=find(y);
if(height[fx]!=height[fy]){
if(height[fx]>height[fy]){
pre[fx]=fy;
}
if(height[fx]<height[fy]){
pre[fy]=fx;
}
}
else{
height[fx]=height[fx]+1;
pre[fy]=fx;
}
}
int main()
{
int m,n,i=0,x,y;
while(cin>>n>>m&&n!=0&&m!=0)
{
i++;
int num=0;
memset(height,0,sizeof(height));
for(int j=1;j<=n;j++)//初始化
{
pre[j]=j;
}
for(int j=1;j<=m;j++)
{
cin>>x>>y;
join(x,y);
}
for(int j=1;j<=n;j++)
{
if(pre[j]==j)
num++;
}
cout<<"Case "<<i<<": "<<num<<endl;
}
return 0;
}
除此之外,在并查集还会遇到成环的情况,压缩路径还有加权标记法,能力有限,有待补充。