目录
并查集作用
1、将两个集合合并。
2、查找两个元素是否在同一个集合中。
并查集思想
1、 每个集合用一棵树表示,集合的编号就是根节点的编号,假如我现在有4个集合
{1,2,3, 4},初始时这四个节点都只有自己,即只有根节点,如下图。
合并操作
将两个不相交的集合合并为一个集合,在并查集中实质为将其中一个元素的根节点(代表性元素)指向另外一个集合的根节点(代表性元素)。
现在我们进行以下几个操作:
这里集合下标都是从1开始的,如初始:index = 1,value = 1,代表每个下标的根节点都为自己
1、合并(2,4),即将集合2合并到集合4里面(需要将2指向4,即把2的父亲节点变为4),合并后:1,4,3,4,其中index = 2 ,value = 4,集合2的根节点指向集合4的根节点,如下图。
2、合并(4,3),合并后:1,4,3,3,index = 4,value = 3,理由同上,如下图。
3、合并(1,3),合并后,3,4,3,3,index = 1,value = 3,如下图。
查找操作
这里Find表示一个集合,Find(x) = 父亲节点的值
1、Find [1] = 3
2、Find [2] = 4
3、Find [3] = 3
显然,如果Find [x] = x的话,那么x为根,在写代码中,如果我们要查找元素x的根节点为多少,只需要while(Find [x] != x)x = Find [x]。
例如,我们查找2的根节点:
x = 2,Find [2] = 4,因为 Find [2] != 2,所以 x = 4 = Find[2]
x = 4, Find [4] = 3,因为 Find [4] != 4,所以 x = 3 = Find [4]
x = 3,Find [3] = 3,因为 Find [3] == 3,所以退出循环,最终x为3,即2的根节点为3,同理2属于集合3。
优化:路径压缩
路径压缩的关键是将路径上的每个节点的父节点设为根节点,以减少树的深度。这样做可以保证后续的查找操作在常数时间内完成,提高了并查集的整体效率。
基本思想:
-
查找操作时的路径压缩: 在执行查找操作时,递归或迭代地找到元素所在集合的根节点。
-
将路径上的每个节点的父节点直接设为根节点: 在查找的过程中,将路径上经过的每个节点的父节点都直接设为根节点,使得整个路径上的节点都直接连接到根节点,从而降低树的深度。
实现方式:
1、递归方式:
在递归方式中,查找的同时进行路径压缩。
int find(int x) {
if (a[x] == x) return x;
return a[x] = find(a[x]); // 路径压缩
}
2、迭代方式:
在迭代方式中,使用循环进行查找,然后再进行路径压缩。
int find(int x) {
int root = x;
while (a[root] != root) {
root = a[root];
}
// 路径压缩
while (a[x] != root) {
int temp = a[x];
a[x] = root;
x = temp;
}
return root;
}
如下图,分别为路径压缩前与路径压缩后。
路径压缩前:
路径压缩后:
此时查找2的根节点不用再通过2->4->3,直接便可以2->3,这便让并查集这个算法时间复杂度近似到O(1),接下来我们通过两个实际的例题来演示算法。
例题一:
这道题来自Acwing的一道算法基础题,链接:836. 合并集合 - AcWing题库,代码如下。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
int a[N];
int find(int x)
{
if(a[x] == x) return a[x];
else return a[x] = find(a[x]);//这里写成return find(a[x])的话就没有路径压缩,可能会超时
}
int main()
{
int n,m;
cin>>n>>m;
for(int i = 1;i<=n;i++)
{
a[i] = i;
}//初始化每个集合的根节点为他本身
for(int i = 0;i<m;i++)
{
char c;
int x,y;
cin>>c>>x>>y;
if(c == 'M')
{
if(find(x) != find(y)) a[find(y)] = find(x);//这里写成了a[y] = x
}
else
{
if(flag(x) == flag(y)) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
}
return 0;
}
例题二:
这道题来自PTA L2-010 排座位,链接:PTA | 程序设计类实验辅助教学平台 (pintia.cn)
代码如下:
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 105;
int parent[MAXN]; // 记录每个宾客的父节点
int relation[MAXN][MAXN]; // 记录每个宾客的关系,1表示朋友,-1表示敌对
int find(int x) {
if (parent[x] == x) return x;
else return find(parent[x]);
}
void unite(int x, int y) {
int root_x = find(x);
int root_y = find(y);
if (root_x != root_y) {
parent[root_x] = root_y;
}
}
int main() {
int N, M, K;
cin >> N >> M >> K;
for (int i = 1; i <= N; ++i) {
parent[i] = i; // 初始化每个宾客的父节点为自己
}
for (int i = 0; i < M; ++i) {
int x, y, relationship;
cin >> x >> y >> relationship;
relation[x][y] = relationship;
relation[y][x] = relationship;
if(relationship == 1) unite(x,y);
}
for(int i = 0;i<K;i++)
{
int x,y;
cin>>x>>y;
if(relation[x][y] == 1) cout<<"No problem"<<endl;
else if(relation[x][y] == 0) cout<<"OK"<<endl;
else if(relation[x][y] == -1 && find(x)==find(y)) cout<<"OK but..."<<endl;
else cout<<"No way"<<endl;
}
}
这里并查集的作用就是查找是否有共同的朋友,即根节点是否相同,例如两个人的关系为敌人的话(-1)如果根节点相同,即有共同的朋友,输出 Ok but ...
本文到此结束,后期有需要补充的地方我会持续补充,有不懂的可以评论区留言,寒假我会持续更新,大家一起加油!