一、并查集的定义
并查集是一种维护集合的数据结构,它的名字中“并”“查”“集”分别取自 Union(合并)、Find(查找)、Set(集合)这 3 个单词。也就是说,并查集支持下面两个操纵:
- 合并:合并两个集合。
- 查找:判断两个元素是否在一个集合。
并查集的实现就是用一个数组:
int father[N]; // 表示元素的父亲结点
例如 father[1]=2 就表示元素 1 的父亲结点是元素 2。另外,如果 father[i]==i,则说明元素 i 是该集合的根结点,但对同一集合来说只存在一个根结点,且将其作为所属集合的标识。
二、并查集的基本操作
总体来说,并查集的使用需要先初始化 father 数组,然后再根据需要进行查找或合并的操作。
1. 初始化
一开始,每个元素都是独立的一个集合,因此需要令所有 father[i] 等于 i。代码如下:
1 // 初始化 2 void init(int n) { 3 int i; 4 for(i=1; i<=n; ++i) { 5 father[i] = i; // 每个元素都是独立的集合 6 } 7 }
2. 查找
由于规定同一集合中只存在一个根结点,因此查找操作就是对给定的结点寻找其根结点的过程。实现思路就是,反复寻找父亲结点,直到找到根结点(即 father[i]==i 的结点)。代码如下:
1 // 对给定的结点寻找其根结点 2 int findFather(int x) { 3 if(x == father[x]) return x; // 如果找到根结点,则返回根结点编号 x 4 else return findFather(father[x]); // 否则,递归判断 x 的父亲结点是否是根结点 5 }
3. 合并
合并是指把两个集合合并成一个集合,题目中一般给出两个元素,要求把这两个元素所在的集合合并。实现步骤如下:
- 对于给定的两个元素 a、b,判断它们是否属于同一集合。可以调用上面的查找函数,对这两个元素 a、b 分别查找根结点,然后再判断其根结点是否相同。
- 合并两个集合:在 1 中已经获得两个元素的根结点 faA 与 faB,因此只需要把其中一个的父结点指向另一个结点。
代码如下:
1 // 把 a,b 所在的集合合并 2 void Union(int a, int b) { 3 int faA = findFather(a); // 查找 a 的根结点 4 int faB = findFather(b); // 查找 b 的根结点 5 if(faA != faB) { // 若 a,b 属于不同集合 6 father[faA] = faB; // 合并它们 7 } 8 }
三、路径压缩
可以在上述 findFather 函数里把当前查询结点的路径上的所有结点的父亲都指向根结点,查找的时候就不需要一直回溯去找父亲了,查询的复杂度可以降为 O(1)。具体步骤如下:
- 按原先的写法获得 x 的根结点 r。
- 重新从 x 开始走一遍寻找根结点的过程,把路径上经过的所有结点的父亲改为根结点 r。
代码如下:
1 int findFather(int x) { 2 int a = x; // 保存原结点 3 while(x != father[x]) { // 寻找根结点 4 x = father[x]; 5 } 6 // 重新走一遍寻找根结点的过程 7 while(a != father[a]) { 8 int z = a; // 保存结点 a 9 a = father[a]; // 回溯父亲结点 10 father[z] = x; // 将所有结点的父亲改为根结点 x 11 } 12 13 return x; // 返回根结点 14 }
下面是一个简单使用并查集的例子。
题目截图:
思路:
需要使用上诉讲的并查集思想,先初始化并查集,然后对输入的每一对好朋友进行合并操作。同时,应设置 isRoot[N] 来记录每个结点是否作为某个集合的根结点,这样当处理完输入数据后就可以累加 isRoot 数组得到集合数目。
代码如下:
1 /* 2 并查集 3 */ 4 5 #include <stdio.h> 6 #include <string.h> 7 #include <math.h> 8 #include <stdlib.h> 9 #include <time.h> 10 #include <stdbool.h> 11 12 #define N 102 13 int father[N]; // 表示元素的父亲结点 14 int isRoot[N] = {0}; // 若为1, 表示为根结点 15 16 // 初始化 17 void init(int n) { 18 int i; 19 for(i=1; i<=n; ++i) { 20 father[i] = i; // 每个元素都是独立的集合 21 isRoot[i] = 1; // 标记结点是根结点 22 } 23 } 24 25 /* 26 // 对给定的结点寻找其根结点 27 int findFather(int x) { 28 if(x == father[x]) return x; // 如果找到根结点,则返回根结点编号 x 29 else return findFather(father[x]); // 否则,递归判断 x 的父亲结点是否是根结点 30 } 31 */ 32 33 int findFather(int x) { 34 int a = x; // 保存原结点 35 while(x != father[x]) { // 寻找根结点 36 x = father[x]; 37 } 38 // 重新走一遍寻找根结点的过程 39 while(a != father[a]) { 40 int z = a; // 保存结点 a 41 a = father[a]; // 回溯父亲结点 42 father[z] = x; // 将所有结点的父亲改为根结点 x 43 } 44 45 return x; // 返回根结点 46 } 47 48 // 把 a,b 所在的集合合并 49 void Union(int a, int b) { 50 int faA = findFather(a); // 查找 a 的根结点 51 int faB = findFather(b); // 查找 b 的根结点 52 if(faA != faB) { // 若 a,b 属于不同集合 53 father[faA] = faB; // 合并它们 54 isRoot[faA] = 0; // 更新标志 55 isRoot[faB] = 1; 56 } 57 } 58 59 int main() { 60 int n, m, i, a, b; 61 scanf("%d %d", &n, &m); // 输入数码宝贝的个数和好朋友组数 62 init(n); // 初始化并查集 63 for(i=0; i<m; ++i) { 64 scanf("%d %d", &a, &b); // 输入每一对朋友 65 Union(a, b); // 合并 66 } 67 int ans = 0; 68 for(i=1; i<=n; ++i) { // 累加得到集合数目 69 ans += isRoot[i]; 70 } 71 printf("%d\n", ans); // 按要求输出 72 73 return 0; 74 }