照例先上题目
8:宗教信仰
-
总时间限制:
- 5000ms 内存限制:
- 65536kB
-
描述
-
世界上有许多宗教,你感兴趣的是你学校里的同学信仰多少种宗教。你的学校有n名学生(0 < n <= 50000),你不太可能询问每个人的宗教信仰,因为他们不太愿意透露。但是当你同时找到2名学生,他们却愿意告诉你他们是否信仰同一宗教,你可以通过很多这样的询问估算学校里的宗教数目的上限。你可以认为每名学生只会信仰最多一种宗教。
输入
-
输入包括多组数据。
每组数据的第一行包括n和m,0 <= m <= n(n-1)/2,其后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 2 3 4 5 4 8 5 8 0 0
样例输出
-
Case 1: 1 Case 2: 7
并查集(Union-find)的实现。附加了两点小技巧,有效地降低了树的高度,使find与union操作的时间复杂度都接近于O(1):
- 在find操作中加入了路径压缩:当前结点原来指向其父,现在指向其爷爷。
- 在union操作中兼顾了树的平衡:总是让规模小的树成为规模大的树的子树。
如果不清楚什么是并查集,我转载了一篇非常好的并查集讲解:并查集(Union-Find)算法介绍;另有我对原文的总结。
代码清单
//并查集Union-find
#include <iostream>
using namespace std;
#define MAXN 50005
int fa_info[MAXN]; //储存结点的父亲在数组中的下标
int size[MAXN]; //每个结点都看成一棵树,那么这棵树有多少个结点?
int find_with_compression(int elem) //带路径压缩的查找,输入待查找的元素,返回它的始祖
{
while (elem != fa_info[elem]) //不是根结点
{
fa_info[elem] = fa_info[fa_info[elem]]; //当前结点原来指向父亲,现在指向爷爷,此为路径压缩
elem=fa_info[elem];
}
return elem;
}
void union_with_balanced(int p, int q, int *set_count)
{
int i=find_with_compression(p); //p的始祖是i
int j=find_with_compression(q); //q的始祖是j
if (i==j) return; //有相同的始祖,说明之前合并过的,直接return
if(size[i]>size[j]) //j树的规模比较小,于是要做i的子树;此为建立平衡树
{
fa_info[j]=i;
size[i] += size[j];
}
else
{
fa_info[i]=j;
size[j] += size[i];
}
--(*set_count); //每合并一次,集合数减1
}
int main()
{
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
int n, m, i, serial=0;
int stu1, stu2;
while (1)
{
cin>>n;
cin>>m;
if(m==0 && n==0) break;
++serial;
//初始化父结点表和树的尺寸表
for (i=0; i<n+1; ++i)
{
fa_info[i]=i;//初始时自己是自己的父亲,有n棵树
size[i]=1;//初始时每棵树的规模都是1
}
for (i=0; i<m; ++i)
{
cin>>stu1;
cin>>stu2;
union_with_balanced(stu1, stu2, &n);
}
cout<<"Case "<<serial<<": "<<n<<endl;
}
return 0;
}