引入
在一些有 N N N 个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。
举个例子:
设初始有若干元素:1,2,3,4,5,6,7,8
元素之间有若干关系:13,24,56,35,78,48
关系合并过程:
初始 {1},{2},{3},{4},{5},{6},{7},{8}
1~3:{1,3},{2},{4},{5},{6},{7},{8}
2~4:{1,3},{2,4},{5},{6},{7},{8}
5~6:{1,3},{2,4},{5,6},{7},{8}
3~5:{1,3,5,6},{2,4},{7},{8}
7~8:{1,3,5,6},{2,4},{7,8}
4~8:{1,3,5,6},{2,4,7,8}
概念
并查集 是一种表示不相交集合的数据结构,用于处理不相交集合的合并与查询问题。在不相交集合中,每个集合通过代表来区分,代表是集合中的某个成员,能够起到唯一标识该集合的作用。一般来说,选择哪一个元素作为代表是无关紧要的,关键是在进行查找操作时,得到的答案是一致的(通常把并查集数据结构构造成树形结构,根节点即为代表)。
在不相交集合上,需要经常进行如下操作:
- findSet(x): 查找元素 x 属于哪个集合,如果 x 属于某一集合,则返回该集合的代表。
- unionSet(x,y): 如果元素 x 和元素 y 分别属于不同的集合,则将两个集合合并,否则不做操作
并查集的实现方法是使用有根树来表示集合——树中的每个结点都表示集合的一个元素,每棵树表示一个集合,每棵树的根结点作为该集合的代表。
并查集基础操作
初始化
现共有 N N N 个元素,对这 N N N 个元素要进行查询与合并操作,现进行初始化;例如 N = 10 N = 10 N=10,初始化方法如下, father[ i i i] 为 i i i 的父结点编号,初始化时结点的父结点为本身,即自己代表自己,建立 N N N 个独立集合:
void Make_Set(int n) {
for (int i = 1; i <= n; i++)
father[i] = i;
}
查询
查询操作是递归查询,在查询某个结点在哪一个集合中时,需沿着其父结点,递归向上,因所属集合代表指向的仍然是其本身,所以可以以 f a t h e r [ x ] = = x father[x] == x father[x]==x 作为递归查询出口。
int Find_Set(int x) {
if (father[x] == x)
return x;
else
return Find_Set(father[x]);
}
合并
在进行集合的合并时,只需将两个集合的代表进行连接即可,即一个代表 作为 另一个代表的父结点。
void Union_Set(int x, int y) {
father[Find_Set(x)] = Find_Set(y);
}
优化
路径压缩
最简单的并查集效率是比较低的。
如果继续进行类似的合并,能会形成一条长长的链,随着链越来越长,我们想要从底部找到根结点会变得越来越难。怎么解决呢?
路径压缩:对于一个集合中的结点,只需要关心它的根结点是谁,不必知道各结点之间的关系(对树的形态不关心),希望每个元素到根结点的路径尽可能短,最好只需要一步,极大地提高了查询效率。
路径压缩需要在查询操作时,把沿途的每个结点的父节点都设为根结点即可。下一次再查询时,就可以节约很多时间。
int Find_Set(int x) {
if (father[x] == x)
return x;
else
return father[x] = Find_Set(father[x]);
}
按秩合并(启发式合并)
由于路径压缩只在查询时进行,每次查询也只压缩一条路径,所以并查集最终的结构仍然可能是比较复杂。
这启发我们:应该把深度低的树往深度高的树上合并,用 r a n k rank rank 数组记录根结点对应树的深度(如果不是根节点,其 r a n k rank rank 相当于以它作为根节点的子树的深度)。一开始,把所有元素的 r a n k rank rank(秩)设为1。合并时比较两个根结点,把 r a n k rank rank 较小者往较大者上合并。
void Make_Set(int n) {
for (int i = 1; i <= n; i++)
father[i] = i, rank[i] = 1;
}
void Union_Set(int x, int y) {
int a = Find_Set(x), b = Find_Set(y);
if (a == b)
return;
if (rank[a] <= rank[b])
father[a] = b;
else
father[b] = a;
if (rank[a] == rank[b])
rank[b]++;
}
例题
这是一道 并查集 的题目,我们首先将所有人的父节点设为自己,如果一对父子的父节点不相同则调用并集函数,在查找时,如果两者父节点相同证明他们两是亲戚,否则不是。
#include<bits/stdc++.h>
using namespace std;
const int Maxn = 2e4 + 5;
int n, m, p, a, b, fa[Maxn], Rank[Maxn];
void Make_Set(){
for(int i = 0;i <= n; ++i)
fa[i] = i, Rank[i] = 1;
return ;
}
int Find_Set(int s) {
return fa[s] == s ? s : (fa[s] = Find_Set(fa[s]));
}
void Union_Set(int s, int e){
int a = Find_Set(s), b = Find_Set(e);
if(a == b)
return;
if(Rank[a] >= Rank[b])
fa[b] = a;
else
fa[a] = b;
if(Rank[a] == Rank[b])
Rank[a] ++;
}
bool Pd_Set(int s, int e){
return Find_Set(s) == Find_Set(e);
}
int main(){
scanf("%d %d", &n, &m);
Make_Set();
for(int i = 1; i <= m; i++) {
scanf("%d %d", &a, &b);
Union_Set(a, b);
}
scanf("%d", &p);
for(int i = 1; i <= p; i++) {
scanf("%d %d", &a, &b);
puts(Pd_Set(a, b) ? "Yes" : "No");
}
return 0;
}
- 银河英雄传说
一条链也是一棵树,只不过是树的特殊形态。因此可以把每一列战舰看作一个集合,用 并查集 维护。最初, N N N 个战舰构成 N N N 个独立的集合。
在没有路径压缩的情况下, f a [ x ] fa[x] fa[x] 就表示排在第 x x x 号战舰前面的那个战舰的编号。一个集合的代表就是位于最前边的那艘战舰。另外,让树上每条边权值为 1,这样树上两点之间的距离 − 1 -1 −1 就是二者之间间隔的战舰数量。
#include<bits/stdc++.h>
using namespace std;
const int Max = 3e4 + 5;
int t, a, b;
int fa[Max], dis[Max], size[Max];
char c;
int Find_Set(int x){
if (fa[x] == x)
return x;
int k = fa[x];
fa[x] = Find_Set(fa[x]);
dis[x] += dis[k];
size[x] = size[fa[x]];
return fa[x];
}
void Make_Set(){
for(int i = 1; i <= Max - 5; i++)
fa[i] = i, size[i] = 1;
}
void Union_Set(int s, int e){
int a = Find_Set(s), b = Find_Set(e);
fa[a] = b;
dis[a] += size[b];
size[a] += size[b];
size[b] = size[a];
}
int main(){
scanf("%d", &t);
Make_Set();
while(t--){
scanf("\n%c", &c);
scanf("%d %d", &a, &b);
if(c == 'M')
Union_Set(a, b);
else{
if (Find_Set(a) == Find_Set(b))
printf("%d\n", abs(dis[a] - dis[b]) - 1);
else
puts("-1");
}
}
return 0;
}
- 食物链
如前面还没有真话,那么无法判断的话就是真话。如果严格按照 A吃B,B吃C,C吃A,把集合划分成 ABC 三类,假设2 1 3是真话,那么到底把 1、3 分别归到A、B还是B、C还是C、A呢,我们无法判断。所以这里我们可以把1分成 1 、 1 + n 、 1 + 2 n 1、1+n、1+2n 1、1+n、1+2n 分别放到 ABC 三个集合中,再来判断正确性。
#include<cstring>
#include<cstdio>
const int Max = 150005;
int fa[Max], a, b, c;
int n, m;
void Make_Set(){
for(int i = 0; i <= 3 * n; i++)
fa[i] = i;
return ;
}
int Find_Set(int s) {
return fa[s] == s ? s : (fa[s] = Find_Set(fa[s]));
}
void Union_Set(int x,int y){
int t1 = Find_Set(x);
int t2 = Find_Set(y);
if(t1 != t2)
fa[t1] = t2;
}
int main(){
scanf("%d %d", &n, &m);
Make_Set();
int ans = 0;
for(int i = 0; i < m; i++){
scanf("%d %d %d", &a, &b, &c);
int x = b - 1;
int y = c - 1;
if(x < 0 || x >= n || y < 0 || y >= n){
ans++;
continue;
}
if(a == 1){
if(Find_Set(x) == Find_Set(y + n) || Find_Set(x) == Find_Set(y + 2 * n))
ans++;
else{
Union_Set(x, y);
Union_Set(x + n, y + n);
Union_Set(x + 2 * n, y + 2 * n);
}
}
else if(a == 2){
if(Find_Set(x) == Find_Set(y) || Find_Set(x) == Find_Set(y + 2 * n))
ans++;
else{
Union_Set(x, y + n);
Union_Set(x + n, y + 2 * n);
Union_Set(x + 2 * n, y);
}
}
}
printf("%d", ans);
return 0;
}