并查集
并查集
基本介绍
数据结构一共分为四大类:集合、线性表、树、图,本文的数据结构就是第一种结构集合。
假设,一个集合即是一个小团伙,每个小团伙都有一个老大,每个团伙成员认识另一个成员,而团队比较大不可能每个成员都互相认识吧,但是他们认识的人去找别的认识的人中最终都可以找到老大,这些人就构成了一个集合。那么这个老大我们称之为”祖宗”,每个人认识的那个人就是它的“父亲“。
有点类似于树的结构,这种寻找一般都是单向的,但是也可以增加数据域保存”孩子“的信息(一般不用)。类似于树,我们就可以像操作树那样,用递归的方式实现寻找(当然也可以非递归)。
核心思想
一个节点一般包含两个信息域,一是自身的信息,二是父亲的信息。
常见用法:定义一个数组,令数组角标为数据的地址,单个数组元素的一个数据域就可以用来存储“父亲“的角标,以实现元素之间的连通。
判断两个元素是否在同一个集合,只需要不断找“父亲”指导找到“祖宗“,判断“祖宗“是否相同即可。
图 方框内是数组角标,圆内是单个数组元素的数据域。图中即为两个集合。
代码实现
初始化
并查集在使用前一定要初始化,让每个节点自己做自己的”父亲”,因为最开始每个元素独立自己是一个集合。
void init(int len) { for (int i = 0; i < len; i++) family[i].parent = i; return; }
找“祖宗“
单纯找
核心原理就是不断的通过数据域的信息去找“父亲”,直到找到一个自己是自己“父亲”的节点即“祖宗”。这个过程可以用递归也可以非递归实现。
1. 递归
int find(int aim) { if (family[aim].parent != aim) aim = find(family[aim].parent); return aim; }
2. 非递归
int find(int x) { while (x != fa[x]) x = fa[x]; return x; }
路径压缩
对于有些题目,实际路径不会对解题有影响,就可以采用路径压缩,其目的就是减少find使用的时间。如上例子,“6”号位找祖宗要找三次,采用路径压缩只需要找一次即可。
int find(int x, int *fa){ if(fa[x] != x) fa[x] = find(fa[x], fa);//查找+路径压缩,如果没有祖先就回溯 return fa[x]; }
合并
合并操作就是检查两个元素是否是同一个集合,如果不是就需要合并在同一个集合。对于谁做谁的父节点一般情况下没有任何关系,因为并查集只是表述集合关系。但是在某些对路径有要求的题目下,就要考虑谁做谁的父节点。
void merge(int a, int b, int *fa) { a = find(a, fa); b = find(b, fa); if (a != b) fa[a] = b; // 如果两个元素不相等就让一个元素成为另一个元素的父节点 return; }
练习题
https://www.luogu.com.cn/problem/P1551 P1551 亲戚
https://www.luogu.com.cn/problem/P1536 P1536 村村通
https://www.luogu.com.cn/problem/P1525 P1525 [NOIP2010 提高组]关押罪(种类并查集)
https://www.luogu.com.cn/problem/P1621 P1621 集合
https://www.luogu.com.cn/problem/P1892 P1892 [BOI2003]团伙
https://www.luogu.com.cn/problem/P1955 P1955 [NOI2015] 程序自动分析(这个有趣)
https://www.luogu.com.cn/problem/P2814 P2814 家谱(这个简单,不过字符串处理一下)
https://vjudge.net/contest/445444 这个并查集题单还是挺有趣的
http://acm.hdu.edu.cn/showproblem.php?pid=3038 How Many Answers Are Wrong(这是一个带权并查集,感兴趣的可以试试)
我的题解
P1551 亲戚 - Kirk~~ - 博客园 (cnblogs.com)
P1536 村村通 - Kirk~~ - 博客园 (cnblogs.com)