进击高手【第十二期】并查集

引入

在一些有 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}

概念

并查集 是一种表示不相交集合的数据结构,用于处理不相交集合的合并与查询问题。在不相交集合中,每个集合通过代表来区分,代表是集合中的某个成员,能够起到唯一标识该集合的作用。一般来说,选择哪一个元素作为代表是无关紧要的,关键是在进行查找操作时,得到的答案是一致的(通常把并查集数据结构构造成树形结构,根节点即为代表)。
在不相交集合上,需要经常进行如下操作:

  1. findSet(x): 查找元素 x 属于哪个集合,如果 x 属于某一集合,则返回该集合的代表。
  2. 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]++;
}

例题

  1. 亲戚(relation)

这是一道 并查集 的题目,我们首先将所有人的父节点设为自己,如果一对父子的父节点不相同则调用并集函数,在查找时,如果两者父节点相同证明他们两是亲戚,否则不是。

#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;
}
  1. 银河英雄传说
    一条链也是一棵树,只不过是树的特殊形态。因此可以把每一列战舰看作一个集合,用 并查集 维护。最初, 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;
}
  1. 食物链
    如前面还没有真话,那么无法判断的话就是真话。如果严格按照 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 11+n1+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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值