本文内容基于《算法笔记》和官方配套练题网站“晴问算法”,是我作为小白的学习记录,如有错误还请体谅,可以留下您的宝贵意见,不胜感激
一、并查集定义
并查集是一种维护集合的数据结构,用于处理一些不相交集合的合并及查询问题,在数据结构中的作用就是判断图中两点的连通性,是一种空间换时间的算法,比搜索来的要更快。构建完成的并查集从其数据结构上来说是树。
1.并查集支持下面两个操作:
(1)合并:合并两个集合
(2)查找:判断两个元素是否在一个集合
2.并查集的实现:并查集通过数组来实现,在数组中散列某结点的父亲节点,形成一个记录父亲结点的静态链表,通过判断父系关系来表示元素所属的集,类似反向遍历的树。对同一个集合来说只存在一个根结点,将其作为所属集合的标识。
二、并查集的基本操作
1.初始化
一开始,每个元素都是独立的一个集合,因此需要令所有father[i]等于i:
2.查找
查找操作就是对给定的结点寻找其根结点的过程,如果两个元素的根结点相同,说明两个元素在同一个集合中。采用递推方法如下:
3.合并
合并是指把两个集合合并成一个集合,合并的过程一般是把其中一个集合的跟结点的父亲指向另一个集合的跟结点。思路如下:
(1)对于给定的两个元素a、b,判断它们是否属于同一集合。可以调用查找函数,对这两个元素a、b分别查找根结点,然后再判断其根结点是否相同。
(2)合并两个集合:在(1)中己经获得了两个元素的根结点faA与faB,因此只需要把其中一个的父亲结点指向另一个结点。例如可以令father[faA]=faB,当然反过来令father[faB]=faA也是可以的,两者没有区别。
三、路径压缩
回忆合并过程,有两个方法,即father[faA]=faB和father[faB]=faA,这两个方法的选择会直接导致最后生成的并查集在形态上大不相同,在极端情况下甚至可能退化成一条链,这样执行查询根结点的算法复杂度会很高。所以书中给出了一种压缩办法,根据并查集的查询操作:寻找某个结点的根结点,可以将某个树中的所有元素的父亲结点都指向根结点,这样查询的复杂度就降到了O(1);
实现思路可以在合并查询的过程中,将子结点到根结点的查询路径上的所有元素的父节点都更改为根结点,具体实现分为两步:
(1)按原先的写法获得x的根结点r;
(2)重新从x开始走一遍寻找根结点的过程,把路径上经过的所有结点的父亲全部改为根结点r;
四、小题练习
1.学校的班级个数
思路:将题干转化为数学语言描述,即若A,B是ans的子集,B,C是ans的子集,则ABC都是ans的子集,求有多少个ans。采用并查集的思想,合并任意关系中两个元素所在的集合,最后计算一共有多少个集合。
实现:并查集+路径压缩
完整代码如下:
#include<cstdio>
const int MAXN = 101;
int father[MAXN] = {};
int findFather(int x) {
int r = x;
while(father[r] != r) r = father[r];
while(x != father[x]) {
int fatherx = father[x];
father[x] = r;
x = fatherx;
}
return r;
}
void Union(int a , int b){
int faA = findFather(a);
int faB = findFather(b);
if(faA != faB) father[faA] = faB;
}
void init(int n) {
for (int i = 1; i <= n; i++) father[i] = i;
}
int main(){
int n , m;
scanf("%d%d", &n , &m);
init(n);
for(int i = 0; i <= m - 1; i++){
int a , b;
scanf("%d%d", &a , &b);
Union(a , b);
}
int ans = 0;
for(int i = 1; i <= n; i++)
if(father[i] == i) ans++;
printf("%d", ans);
}
2.学校的班级人数
思路:开一个散列表记录对应根结点(即集合或者班级)的班级人数,随着关系的合并,不断更新根结点的班级人数,最后开一个新的数组记录根结点的值,并进行降序排序;
实现:并查集+路径压缩+sort
完整代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 101;
int father[MAXN] = {};
int num[MAXN] = {}; //散列记录个数
int redution[MAXN] = {};
bool cmp(int a , int b){
return a > b;
}
int findFather(int x) { //查询父结点
int r = x;
while(father[r] != r) r = father[r];
while(x != father[x]) { //路径压缩s
int fatherx = father[x];
father[x] = r;
x = fatherx;
}
return r;
}
void Union(int a , int b){ //合并两个集合
int faA = findFather(a);
int faB = findFather(b);
if(faA != faB) {
num[faB] += num[faA]; //注意谁是父结点
father[faA] = faB;
}
}
void init(int n) { //初始化
for(int i = 1; i <= n; i++) {
father[i] = i;
num[i] = 1;
}
}
int main(){
int n , m;
scanf("%d%d", &n , &m);
init(n);
for(int i = 0; i <= m - 1; i++){
int a , b;
scanf("%d%d", &a , &b);
Union(a , b);
}
int ans = 0;
int index = 0;
for(int i = 1; i <= n; i++)
if(father[i] == i) {
ans++;
redution[index++] = num[i];
}
printf("%d\n", ans);
sort(redution , redution + index , cmp);
int countr = 1;
for(int i = 0; i <= index - 1; i++){
printf("%d", redution[i]);
if(countr < index) {
printf(" ");
countr++;
}
}
}
3.是否相同班级
思路:在实现并查集后,对提供的k组关系进行并查集的查询,如果两个元素的根结点相同,说明在一个班级(集合)内;
实现:并查集+路径压缩;
完整代码如下:
#include<cstdio>
const int MAXN = 101;
int father[MAXN] = {};
int findFather(int x) {
int r = x;
while(father[r] != r) r = father[r];
while(x != father[x]) {
int fatherx = father[x];
father[x] = r;
x = fatherx;
}
return r;
}
void Union(int a , int b){
int faA = findFather(a);
int faB = findFather(b);
if(faA != faB) father[faA] = faB;
}
void init(int n) {
for (int i = 1; i <= n; i++) father[i] = i;
}
int main(){
int n , m;
scanf("%d%d", &n , &m);
init(n);
for(int i = 0; i <= m - 1; i++){
int a , b;
scanf("%d%d", &a , &b);
Union(a , b);
}
int k;
scanf("%d", &k);
for(int i = 0; i <= k - 1; i++){
int a , b;
scanf("%d%d", &a , &b);
if(findFather(a) == findFather(b)) printf("Yes\n");
else printf("No\n");
}
}
4.迷宫连通性
思路:这道题就是并查集在数据结构中的体现,在没有学习并查集之前,判断连通性需要暴力搜索,这样时间复杂度会很高,在学习了并查集之后,可以将所有可以连通的点通过并查集合并在一起,这样生成的是一棵树。所以,一个并查集内存放的就是可以连通的全部元素,这是图内的一个重要算法。
实现:并查集+路径压缩
完整代码如下:
#include<cstdio>
const int MAXN = 101;
int father[MAXN] = {};
int findFather(int x) {
int r = x;
while(father[r] != r) r = father[r];
while(x != father[x]) {
int fatherx = father[x];
father[x] = r;
x = fatherx;
}
return r;
}
void Union(int a , int b){
int faA = findFather(a);
int faB = findFather(b);
if(faA != faB) father[faA] = faB;
}
void init(int n) {
for (int i = 1; i <= n; i++) father[i] = i;
}
int main(){
int n , m;
scanf("%d%d", &n , &m);
init(n);
for(int i = 0; i <= m - 1; i++){
int a , b;
scanf("%d%d", &a , &b);
Union(a , b);
}
int ans = 0;
for(int i = 1; i <= n; i++)
if(father[i] == i) ans++;
if(ans > 1) printf("No");
else printf("Yes");
}
5.班级最高分
思路:这道题和第2题有点类似,可以开一个散列表记录对应根结点(集合或班级)的最高分数,随着并查集的合并,更新根结点的最高分数,最后开一个新的数组记录根结点的值,并进行降序排序;
实现:并查集+路径压缩+sort
完整代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 101;
int father[MAXN] = {};
int score[MAXN] = {}; //先记录该结点该结点为根结点对应的分数,然后更新对应的最大的分数
int redution[MAXN] = {};
bool cmp(int a , int b){
return a > b;
}
int findFather(int x) {
int r = x;
while(father[r] != r) r = father[r];
while(x != father[x]) {
int fatherx = father[x];
father[x] = r;
x = fatherx;
}
return r;
}
void Union(int a , int b){
int faA = findFather(a);
int faB = findFather(b);
if(faA != faB) {
score[faB] = max(score[faA] , score[faB]); //注意是以谁为父结点
father[faA] = faB; //以faB为父结点
}
}
void init(int n) {
for (int i = 1; i <= n; i++) father[i] = i;
}
int main(){
int n , m;
scanf("%d%d", &n , &m);
init(n);
for(int i = 1; i <= n; i++)
scanf("%d", score + i);
for(int i = 0; i <= m - 1; i++){
int a , b;
scanf("%d%d", &a , &b);
Union(a , b);
}
int ans = 0;
int index = 0;
for(int i = 1; i <= n; i++)
if(father[i] == i) {
ans++;
redution[index++] = score[i];
}
printf("%d\n", ans);
sort(redution , redution + index , cmp);
int countr = 1;
for(int i = 0; i <= index - 1; i++){
printf("%d", *(redution + i));
if(countr < index){
printf(" ");
countr++;
}
}
}
备注
1.能用递推实现就不要用递归,递归存在爆栈的可能性,不稳定性比较大;
2.一个并查集内存放的是无向图中可以连通的全部结点。