【数据结构】并查集

  • ~4.65K 字
  1. 1. 并查集
    1. 1.1. 概念
    2. 1.2. 实现1
      1. 1.2.1. 效率
    3. 1.3. 实现2
      1. 1.3.0.1. 每个集合用一颗有根树表示
    4. 1.3.1. 效率
    5. 1.3.2. 改进
  2. 1.4. 路径压缩
    1. 1.4.1. 代码
  3. 1.5. 模板
    1. 1.5.1. 题目描述
    2. 1.5.2. 输入格式
    3. 1.5.3. 输出格式
    4. 1.5.4. 样例 #1
    5. 1.5.5. 样例输入 #1
    6. 1.5.6. 样例输出 #1
    7. 1.5.7. 提示
  4. 1.6. 种类并查集

并查集

概念

不相交集合:若有n个元素,其中的每个元素如果只属于某个集合,则可以把他们划为不想交集合

问题描述:将编号分别为1-n的n个对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在集合

解决的问题:

  • 合并两个集合
  • 查找某个元素属于哪个集合

定义一个数组set,set[i]代表i所在的集合

我们用每个集合编号最小的元素标记所在集合

实现1

如set: 1 2 1 4 2 6 1 6 2 2

他的不相交集合为{1,3,7}, {4}, {2,5,9,10}, {6,8}

效率

查:只需要返回下标对应元素,因为此时对应的就是集合中的最小元素,

1
2
3
find1(x){
return set[x];
}

并:合并两个集合后,要找出一个更小的作为代表元素,合并时要遍历set中所有元素,把还是原来元素的改成新的更小元素,

1
2
3
4
5
6
7
merge1(a,b){
i = min(a,b);
j = max(a,b);
for (int k=1;k<=n;k++){
if (set[k]==j) set[k]==i;
}
}

实现2

每个集合用一颗有根树表示

定义一个数组set

  • 若set[i]=i, 则i为该树的根节点,i代表本集合
  • 若set[i]=j, 若ji, 则j是i的父节点

效率

查:如果集合对应的不是自己,则自己上面有父节点,将父节点存下来继续查找直到查到根节点,最坏,一般

1
2
3
4
5
6
7
find2(x){
r = x;
while(set[r]!=r){
r = set[r];
}
return r;
}

并:只需要将根节点改为另一个集合的根节点即可,

1
2
3
merge2(a,b){
set[a]=b;
}

改进

如何避免最坏情况?

方法:将深度小的树合并到深度大的树,合并后深度不会变化

效果:合并后,包含k个节点的树最大高度不超过

代码: 在合并时记录每棵树的高度,谁的高度大谁是新的根节点

1
2
3
4
5
6
7
8
merge3(a,b){
if (height(a)==height(b)){
height(a)=height(a)+1;
set[b]=a;
}
else if (height(a)<height(b)) set[a]=b;
else set[b]=a;
}

此时查找操作的时间复杂度变成了

路径压缩

问题:若一棵树的高度很大,查找路径很长,查找量很大的时候,很容易tle

思想:查找时如果路径较长,则修改信息,让下次查找的时候速度更快

方案:

  • 找到根节点
  • 修改查找路径上的所有节点,将它们都指向根节点

代码

1
2
3
4
find3(x){
if (set[x]!=x) set[x]=find3(set[x]); //当不是根节点时,调用find3找到其父节点的父节点,并赋给他的父节点,此时x的父节点改为他的父节点的父节点,直到指到他的根节点
return set[x];
}

模板

题目描述

如题,现在有一个并查集,你需要完成合并和查询操作。

输入格式

第一行包含两个整数 ,表示共有 个元素和 个操作。

接下来 行,每行包含三个整数 。

当 时,将 与 所在的集合合并。

当 时,输出 与 是否在同一集合内,是的输出 Y ;否则输出 N 。

输出格式

对于每一个 的操作,都有一行输出,每行包含一个大写字母,为 Y 或者 N 。

样例 #1

样例输入 #1

1
2
3
4
5
6
7
8
4 7
2 1 2
1 1 2
2 1 2
1 3 4
2 1 4
1 2 3
2 1 4

样例输出 #1

1
2
3
4
N
Y
N
Y

提示

对于 的数据,,。

对于 的数据,,。

对于 的数据,,,,。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
template<typename T>
class DSU{
public:
DSU(T n){
this->n = n;
disj.resize(n+1);
cnt.resize(n+1);
iota(disj.begin()+1, disj.end(), (T)1);
fill(cnt.begin()+1, cnt.end(), (T)1);
}
// 查找操作,使用路径压缩
T find(T x){
if(disj[x]!=x) disj[x]=find(disj[x]);
return disj[x];
}
// 合并操作,寻找两者最小根节点并将大的修改成小的
void merge(T a,T b){
T ra = find(a);
T rb = find(b);
if(cnt[rb]<cnt[ra]) swap(ra,rb);
if(ra!=rb){
disj[ra] = rb;
cnt[rb]+=cnt[ra];
}
}
// 找集合个数(并查集找根)
T fcnt(){
vector<bool> vis(n+1, false);
T ans = 0;

for (int i = 0;i<=n;i++){
if (!vis[find(i)]){
vis[find(i)]=1;
ans++;
}
}
return ans;
}
private:
vector<T> disj;
vector<T> cnt;
vector<bool> vis;
T n;
};


int n, m;
int main(){
cin >> n >>m;
DSU<int> dsu(n);
while(m--){
int z, x, y;
cin>>z>>x>>y;
// 进行合并
if (z==1){
dsu.merge(x,y);
}
else {
// 如果两者的根节点相同,则属于同一集合
if(dsu.find(x)==dsu.find(y)) cout << "Y"<<endl;
else cout << "N"<<endl;
}
}
return 0;

}

种类并查集

一般的并查集维护的是具有连通性的一组关系,而对于一个具有多种关系(相斥或相互独立),我们需要使用种类并查集,亦称拓展域并查集

种类并查集是一种数据结构,用于解决具有多个相互关系集合的问题。它是传统并查集的扩展,能够处理集合间的不同关系,如相互排斥或相互独立的关系。

例:

该题使用贪心,可以把矛盾关系从大到小排序,将矛盾关系大的关到不同监狱中

但给出的信息是存在仇恨不能出现在同一监狱的两个人,故不能用传统并查集的思路处理

这里我们开两倍大小的并查集(拓展域),用前一半维护朋友关系,后一半维护敌人关系

假如共有4人,1是2的敌人,那我们将1和2+4合并,1+4和2合并,即1和6是敌人,5和2是敌人,又5和6是敌人关系,故1和5是朋友,5又和2是敌人,故1和2是敌人,2和1同理

故我们需要每次merge(i,j+n)与merge(i+n,j)来建立关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 2e4 + 10;
const int mod = 1e9 + 7;
struct node{
int x, y, z;
}pri[100050];
// 为假想敌预留空间,开2*N
int disj[2*N];
//查
int find(int x){
if(disj[x]!=x)disj[x]=find(disj[x]);
return disj[x];
}
// 并
void merge(int a, int b){
if (find(a)>find(b))disj[find(a)] = find(b);
else disj[find(b)]=find(a);
}

void solve() {
int n,m;
cin >> n >> m;
for (int i =1;i<=m;i++){
cin >> pri[i].x >> pri[i].y >> pri[i].z;
}
// 按罪恶值从大到小排序
sort(pri+1,pri+m+1, [&](const node a, const node b){return a.z>b.z;});
for (int i = 1;i<=n*2;i++){
disj[i]=i;
}
for (int i =1;i<=m;i++){
if (find(pri[i].x)==find(pri[i].y)){ // 若敌人的敌人仍在同监狱里,那还是敌人,只能输出值
cout <<pri[i].z;
return ;
}
// 合并敌人的敌人
merge(pri[i].x+n,pri[i].y);
merge(pri[i].x,pri[i].y+n);
}
cout << 0; // 全部遍历后发现没有符合的
}
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int tomorin=1;
//cin >> tomorin;
while (tomorin--) solve();
return 0;
}
赞助喵
非常感谢您的喜欢!
赞助喵