目录
1.并查集
1.1并查集的作用
- 并(Union):将两个不相交的集合合并成一个集合。
- 查(Find):确定某个元素属于哪个集合,通常返回该集合的代表元素。
1.2基本思想
并查集通常使用数组p来实现,数组中的每个元素代表一个节点,数组的值表示该节点的父节点。初始时,每个节点的父节点是它自身,表示每个节点单独构成一个集合。
1.3示例说明
🌰假设我们有一个包含 6 个节点的并查集,节点编号从 0 到 5。
-
初始化:
在初始化阶段,每个节点的父节点都是它自身。我们使用一个数组
parent
来存储每个节点的父节点信息,此时parent
数组的初始状态为:
parent[0] = 0;
parent[1] = 1;
parent[2] = 2;
parent[3] = 3;
parent[4] = 4;
parent[5] = 5;
可以将其理解为每个节点都独立构成一个集合,此时有 6 个集合,每个集合只有一个元素。
-
合并操作:
🌰假设我们要合并节点 0 和 1 所在的集合。
- 首先,查找节点 0 的根节点,通过
parent[0]
发现它的父节点是 0,所以 0 就是它所在集合的根节点。 - 接着,查找节点 1 的根节点,
parent[1] = 1
,所以 1 是它所在集合的根节点。 - 由于 0 和 1 是不同的根节点,我们将其中一个根节点(比如 0)的父节点设置为另一个根节点(1),即
parent[0] = 1
。此时parent
数组变为:
- 首先,查找节点 0 的根节点,通过
parent[0] = 1;
parent[1] = 1;
parent[2] = 2;
parent[3] = 3;
parent[4] = 4;
parent[5] = 5;
现在节点 0 和 1 属于同一个集合,这个集合的根节点是 1。
🌰再假设我们要合并节点 2 和 3 所在的集合:
- 查找节点 2 的根节点,
parent[2] = 2
,根节点是 2。 - 查找节点 3 的根节点,
parent[3] = 3
,根节点是 3。 - 将 2 的父节点设置为 3,即
parent[2] = 3
,parent
数组变为:
parent[0] = 1;
parent[1] = 1;
parent[2] = 3;
parent[3] = 3;
parent[4] = 4;
parent[5] = 5;
此时节点 2 和 3 属于同一个集合,根节点是 3。
-
查找操作:
比如我们要查找节点 0 所在集合的根节点。
- 首先访问
parent[0] = 1
,发现 1 不是根节点(因为parent[1] != 1
不成立)。 - 接着访问
parent[1] = 1
,此时 1 是根节点,所以节点 0 所在集合的根节点是 1。
- 首先访问
-
判断连通性:
判断节点 0 和 1 是否连通,通过查找它们的根节点,发现都是 1,所以节点 0 和 1 是连通的。
判断节点 0 和 2 是否连通,查找节点 0 的根节点是 1,查找节点 2 的根节点是 3,根节点不同,所以节点 0 和 2 不连通。
1.4优化:路径压缩
📍路径压缩的核心思想:
在查找元素的过程中,将该元素到根节点路径上的所有节点直接连接到根节点,从而减少后续查找操作的时间复杂度。
具体来说,当我们查找一个元素的根节点时,在递归返回的过程中,将该元素及其路径上的所有祖先节点的父节点都直接设置为根节点。
2.例题:合并集合
2.1题目描述
一共有 n个数,编号是 1∼n,最开始每个数各自在一个集合中。
现在要进行 m 个操作,操作共有两种:
M a b
,将编号为 aa 和 bb 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;Q a b
,询问编号为 aa 和 bb 的两个数是否在同一个集合中;输入格式
第一行输入整数 n 和 m。
接下来 m行,每行包含一个操作指令,指令为
M a b
或Q a b
中的一种。输出格式
对于每个询问指令
Q a b
,都要输出一个结果,如果 aa 和 bb 在同一集合内,则输出Yes
,否则输出No
。每个结果占一行。
数据范围
1≤n,m≤ \(10^{5}\)
输入样例:
4 5 M 1 2 M 3 4 Q 1 2 Q 1 3 Q 3 4
输出样例:
Yes No Yes
2.2代码展示
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int parent[N];
int n, m;
// 📍查找祖宗结点(优化:路径压缩)📍
int find(int x) {
if (x != parent[x]) {
parent[x] = find(parent[x]);
}
return parent[x];
}
//合并
void merge(int x,int y) {
int px = find(x);
int py = find(y);
if (px != py) {
parent[px] = py;
}
}
//查找
void check(int x, int y) {
int px = find(x);
int py = find(y);
if (px == py) cout << "Yes" << endl;
else cout << "No" << endl;
}
void cal() {
//初始化操作:每个结点的父节点都为自身,表示每个节点单独构成一个集合
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
for (int i = 0; i < m; i++) {
char ch;
int x, y;
cin >> ch >> x >> y;
if (ch == 'M') merge(x, y);
else check(x, y);
}
}
signed main() {
cin >> n >> m;
cal();
return 0;
}
3.例题:连通块中点的数量
3.1题目描述
给定一个包含 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
中的一种。输出格式
对于每个询问指令
Q1 a b
,如果 a 和 b 在同一个连通块中,则输出Yes
,否则输出No
。对于每个询问指令
Q2 a
,输出一个整数表示点 a 所在连通块中点的数量每个结果占一行。
数据范围
1≤n,m≤ \(10^{5}\)
输入样例:
5 5 C 1 2 Q1 1 2 Q2 1 C 2 5 Q2 5
输出样例:
Yes 2 3
3.2代码展示
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int parent[N], Size[N]; // 修改为小写的 size
int n, m;
// 查找元素 x 所在集合的代表元,并进行路径压缩⚠️
int find(int x) {
if (x != parent[x]) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// 合并元素 x 和 y 所在的集合
void merge(int x, int y) {
int px = find(x);
int py = find(y);
if (px != py) {
parent[px] = py;
Size[py] += Size[px]; //📍📍📍
}
}
// 检查元素 x 和 y 是否在同一集合
void check(int x, int y) {
int px = find(x);
int py = find(y);
if (px == py) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
}
// 处理输入并执行相应操作
void cal() {
// 初始化每个元素的父节点为自身,集合大小为 1
for (int i = 1; i <= n; i++) {
parent[i] = i;
Size[i] = 1; //📍📍📍
}
// 处理 m 次操作
for (int i = 0; i < m; i++) {
string str;
int x, y;
cin >> str;
if (str == "C") {
cin >> x >> y;
merge(x, y);
} else if (str == "Q1") {
cin >> x >> y;
check(x, y);
} else if (str == "Q2") {
int node;
cin >> node;
// 输出 node 所在集合的代表元对应的集合大小
cout << Size[find(node)] << endl; //Size[node]不正确的!!⚠️
}
}
}
signed main() {
cin >> n >> m;
cal();
return 0;
}