题目
描述
世界上有许多宗教,你感兴趣的是你学校里的同学信仰多少种宗教。
你的学校有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
分析
“信仰同一宗教”这一关系是一个等价关系(自反、传递、对称),因此,很容易想到,最大的信仰数量是这一等价关系所导出的等价类的数量。
看到这里,立刻明白这道题是非常原始的并查集问题。
并查集
并查集是一种高效的记录、查询等价类的方式,它把属于同一个等价类的集合通过前驱记录的形式构成一棵树,并用根节点来代表整个等价类。
并查集支持Find(查询)、Merge(合并)两种基本的操作。
- Find(i)通过迭代前驱寻找节点 i 的祖宗,也就是它所在树的根——其所在等价类的代表元。通常,我们用路径压缩来提高性能。
- Merge(i, j)将 i 节点和 j 节点各自所在的等价类合并成同一等价类。它非常简单:直接将把Find(j)的前驱设置为Find(i)即可。
在并查集中,路径压缩是一项非常巧妙的设计,我们在Find的同时,将迭代(或递归)查询路径上的节点都改为直接挂在根之下,这样,不仅不影响的查询的正确性,又缩减树的深度,使得后续的Find操作的代价趋近于一个常数。
我会在代码实现中,实现一个基本的并查集模板。
代码实现
#include <stdio.h>
#pragma warning(disable: 4996) //make Visual Studio happy
class MergeFindSet
{
private:
int* parents;
public:
const int size;
public: