并查集主要用于处理不相交集合的合并问题。
并查集:将编号为1 ~ n的n个的对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在集合,在这个集合中,并查集的操作有初始化、合并、查找。
(1)初始化:
定义数字int s[]是以结点i为元素的并查集,刚开始每个点属于独立的集合,且以元素的值表示它的集s[i]
(2)合并:
把两个集合合并为一个集合,以此形成一棵搜索树
(3)查找:
查找元素是一个递归的过程,直到元素的值和它的集相等就可以找到根结点的集。
(4)统计有多少个集
如果s[i] = i,这是一个根结点,是它所在集的代表,统计根结点的数量就是集的数量。
(5)压缩路径:
在查询过程中,查询元素i所属的集需要搜索路径找到根结点,返回的结果是根结点,这条搜索路径可能很长,如果在返回时顺便把i所属的集改成根结点,就能在O(1)时间内得到结果,此方法成为压缩路径,即将整个搜索路径上的元素所属的集全改为根结点。
//算法核心
int find(int x)
{
if(x != s[x]) s[x] = find(s[x]);
return s[x];
}
模板题
合并集合
一共有n个数,编号是1~n,最开始每个数各自在一个集合中。
现在要进行m个操作,操作共有两种:
“M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
“Q a b”,询问编号为a和b的两个数是否在同一个集合中;
输入格式
第一行输入整数n和m。
接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种。
输出格式
对于每个询问指令”Q a b”,都要输出一个结果,如果a和b在同一集合内,则输出“Yes”,否则输出“No”。
每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
#include <iostream>
#include<string.h>
using namespace std;
const int N = 100010;
int n, m;
int s[N];
int find(int x)//返回父亲结点 + 路径压缩
{
if(s[x] != x) //s[x]不是x的父结点
s[x] = find(s[x]);//路径压缩,回溯时全部指向一个父结点
return s[x];//元素值=集值,返回x的父结点
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++ ) s[i] = i;
while(m -- )
{
char op[2];
int a, b;
scanf("%s%d%d", op, &a, &b);//scanf读取字符数组用%s会避免读入空格和回车
if(op[0] == 'M') s[find(a)] = find(b);//合并 让s[x] = y,就是y的父结点变为s[x];
else
{
if(find(a) == find(b)) puts("Yes");//这里就用到了压缩路径
else puts("No");
}
}
return 0;
}
加深理解
举个简单例子,一群人在一起聊天,a认识b,b认识c,c认识d,输入你想让对方认识的两个人,比如a和d,如果他们之间有朋友联系,那么a可以和d打个招呼(“Nice to meet you !”),如果两个之间相互独立,输出"Sorry!I don’t know you!"
#include<iostream>
using namespace std;
const int N = 10010;
int s[N];
int find(int x)
{
if(s[x] != x) s[x] = find(s[x]);
return s[x];
}
int main()
{
int n, m;
cin >> n >> m;//输入总人数和多少对认识的人
for(int i = 1 ; i <= n; i ++ ) s[i] = i;
while( m -- )
{
int x, y;
cin >> x >> y;//比如a认识b
s[find(x)] = find(y);//合并到一起
}
int a, b;
cin >> a >> b;//输入你想让谁和谁打招呼
if(find(a) == find(b)) cout << "Nice to meet you !";
else cout << "Sorry!I don't know you!";
}
并查集简单题
Ubiquitous Religions(无处不在的宗教)
描述
当今世界上有太多不同的宗教,很难一一掌握。您有兴趣找出您大学中有多少不同宗教信仰的学生。
您知道您的大学中有n个学生(0 <n <= 50000)。向每个学生询问他们的宗教信仰是不可行的。此外,许多学生不愿意表达自己的信念。避免这些问题的一种方法是,问m(0 <= m <= n(n-1)/ 2)对学生,并询问他们是否信仰同一宗教(例如,他们可能知道他们是否都参加同一宗教)教会)。从这些数据中,您可能不知道每个人的信仰,但是您可以大致了解在校园中可以代表多少种宗教。您可以假设每个学生最多订阅一种宗教。
输入值
输入包含多种情况。每种情况都以指定整数n和m的行开头。接下来的m行分别由两个整数i和j组成,指定学生i和j信仰相同的宗教。学生从1到n编号。输入的结尾由其中n = m = 0的行指定。
输出量
对于每个测试用例,在一行上打印例号(以1开头),然后是大学学生所信奉的不同宗教的最大数量。
样本输入
10 9
1 2
1 3
1 4
1 5
1 6
1 7
1 8
1 9
1 10
10 4 4
2 3
4 5
4 8
5 8
0 0
样本输出
Case 1: 1
Case 2: 7
思路:输入人数,开始每个人是一个集合,将信仰宗教相同的人合并,然后总集合数减去合并的就是学生信奉的不同宗教的最大数量
#include<iostream>
using namespace std;
const int N = 50050;
int s[N];
int n, m;
int find(int x)
{
if(s[x] != x) s[x] = find(s[x]);
return s[x];
}
int main()
{
int x, y;
int cnt = 0;
while(scanf("%d%d", &n, &m))
{
if(n == 0 && m == 0) break;
cnt ++ ;
int ans = 0;
for(int i = 1; i <= n; i ++ ) s[i] = i;
while(m -- )
{
scanf("%d%d",&x, &y);
x = find(x);
y = find(y);
if(x != y)//xy属于不同集合
{
s[x] = y;//合并
ans ++ ;
}
}
printf("Case %d: %d\n", cnt, n - ans);
}
return 0;
}