【算法编程】并查集
并查集是一个非常简单高效的数据结构,其可以快速地对集合进行合并和查询,通常可用于集合的合并、图连通域的合并等。一个对并查集非常形象的解释可参见算法学习笔记(1) : 并查集
基本原理:
- 给定一组集合,每个集合可以用一棵树表示,树根的编号可以表示为整个集合的编号,每个结点保存其父结点的编号。可以维护一个数组 F [ i d x ] F[idx] F[idx],其表示编号为第 i d x idx idx 个结点的父节结点。
- 初始化时,每个结点 i d x idx idx 自身可以独立为一个集合,即定义为 F [ i d x ] = i d x z F[idx] = idxz F[idx]=idxz
- 进行集合的合并时,如果两个集合不是同一个,则只需要将所在的两个集合的根结点进行合并即可。
基本操作:
- 判断当前结点是不是所在树的根结点,只需要判断 i d x idx idx 是否与 F [ i d x ] F[idx] F[idx] 相等;
- 获取某个元素所在集合的编号(即获得结点所在树的根结点),只需要不断地根据数组 F F F 向上查询即可,当满足 i d x = = F [ i d x ] idx == F[idx] idx==F[idx] 时即可;
- 将 a , b a, b a,b 分别所在的集合进行合并,只需要将其所在的两个根结点合并即可。
例如给定已有的两个集合 { 1 , 2 , 3 , 4 } , { 5 , 6 , 7 } \{1,2,3,4\}, \{5, 6, 7\} {1,2,3,4},{5,6,7},其分别可以表示为树结构,合并时候,只需要合并两个根结点即可:
优化:
- 并查集是一种树结构,因此可能会退化为很长的单链表,因此对于离根结点较远的结点,每次都需要不断地向上查询。一种简单的优化方法叫做路径压缩,即将每个结点直接与根结点相连,代码可以表示为:
// 查询编号为x的结点所在的树的根结点编号
int find(int x) {
// 路径压缩,每次在递归时,每个结点的父结点直接指向根结点
if(x != F[x]) F[x] = find(F[x]);
return F[x];
}
- 另外还有按秩合并,即在对集合进行合并的时候,将深度小的子树合并到深度大的子树上
应用1:集合的合并与查询
一共有n个数,编号是1~n,最开始每个数各自在一个集合中。现在要进行m个操作,操作共有两种:
“M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
“Q a b”,询问编号为a和b的两个数是否在同一个集合中;
输入输出要求:第一行输入整数n和m。接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种。
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int F[N]; // 模拟并查集
// 初始化每个元素各自代表一个集合
void init(int n) {
for(int i = 1; i <= n; i ++) F[i] = i;
}
// 寻找x所在的集合编号
int find(int x) {
if(x != F[x]) F[x] = find(F[x]); // 路径压缩
return F[x];
}
int main() {
int n, m;
scanf("%d%d", &n, &m);
init(n);
// 接下来m个操作
while(m --) {
char q;
int a, b;
scanf("%s%d%d", &q, &a, &b);
if(q == 'M') F[find(a)] = find(b);
else {
if(find(a) == find(b)) printf("Yes\n");
else printf("No\n");
}
}
return 0;
}
应用2:查询连通子图的结点个数
给定一个包含n个点(编号为1~n)的无向图,初始时图中没有边。现在要进行m个操作,操作共有三种:
“C a b”,在点a和点b之间连一条边,a和b可能相等;
“Q1 a b”,询问点a和点b是否在同一个连通块中,a和b可能相等;
“Q2 a”,询问点a所在连通块中点的数量;
输入输出要求:第一行输入整数n和m。接下来m行,每行包含一个操作指令,指令为“C a b”,“Q1 a b”或“Q2 a”中的一种。
思路: 使用并查集建模,当两个结点连通时,则说明这两个结点所在的连通域(集合)进行合并。维护一个F数组,用于记录x元素的父结点F[x],当且仅当F[X]==X时说明其为根结点;维护一个数组cnt,用于保存每个元素作为根结点时其所包含所有结点的个数
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int F[N]; // 并查集
int cnt[N]; // 用于记录每个结点作为根结点时集合的元素个数
// 初始化F,别忘了初始化cnt(每个元素自己代表一个连通域,元素为1)
void init(int n) {
for(int i = 1; i <= n; i ++) F[i] = i, cnt[i] = 1;
}
int find(int x) {
if(x != F[x]) F[x] = find(F[x]);
return F[x];
}
int main() {
int n, m;
scanf("%d%d", &n, &m);
init(n);
while(m --) {
char op[2];
int a, b;
scanf("%s", op);
if(*op == 'C') {
scanf("%d%d", &a, &b);
int aa = find(a), bb = find(b);
F[aa] = bb; // 将a所在的连通图与b所在的连通图进行合并
if(aa != bb) cnt[bb] += cnt[aa]; // 如果a和b不在一个连通图里,则将a和b对应的元素个数累计起来
}else if(op[1] == '1') {
scanf("%d%d", &a, &b);
if(find(a) == find(b)) printf("Yes\n");
else printf("No\n");
}else {
scanf("%d", &a);
printf("%d\n", cnt[find(a)]);
}
}
return 0;
}