5.1 并查集

5.1 并查集

并查集是一种精巧且实用的数据结构,主要用于处理一些不相交集合的合并问题。

如果还记得紫书十一章内容的同学,这个概念肯定不是第一次见了,在讲解Kruskal算法时就已经简单介绍过这种数据结构:其中图上的结点分割成一个一个不相交的点集合,并查集将一个集合中的点用一种类似于链表(递归)的方法存储着,集合中的点不存在次序关系,只存在是否属于某个“帮派”的属性,优势就在于两个集合的合并就像链表的插入一样非常之快。当时也提到了并查集可能遇到的特殊情况:树退化成一条长链子从而降低查找速度。解决的方法是在遍历的同时,将遍历过的结点都改成树根的子节点,就可以增加下次查找的效率。

Kruskal算法:

int cmp(const int i,const int j){
   return w[i]<w[j];}//w表示权重
int find(int x){
   return p[x]==x?x:p[x]=find(p[x]);}//返回集合编号
//但如果只为了得到集合(树)的编号(根),不需要有赋值的操作:p[x]=find(p[x])
//赋值的操作就是前面提到的对特殊情况的优化:将遍历过的点都改成树根的子节点
int Kruskal(){
   int ans=0; for (int i=0;i<n;i++) p[i]=i;//建立并查集 
	for (int i=0;i<m;i++) r[i]=i;//给每条边配上序号,排序后将第i小的边的序号保存在r[i]
	//排序的关键字是对象的代号,而不是对象本身,这被称为间接排序
	sort(r,r+m,cmp);//进行排序,u[i]和v[i]分别表示编号为i的结点的两个端点的序号 
	for (int i=0;i<m;i++){
   int x=find(u[r[i]]),y=find(v[r[i]]);
		if (x!=y) {
   ans+=w[r[i]]; p[x]=y;}//ans+=w[r[i]]相当于加上边的权重,这个很好理解
		//p[x]=y相当于将v结点所在树接到u结点的根上(因为不考虑树的形态,只要保证连通即可) 
	} return ans; 
} 

这里我们接着黑书的内容从基本的操作更加细致的了解并查集。


并查集操作的简单实现

首先我们先设定一个简单的背景:在一个城市中有n个人,他们组成不同的“帮派”,给出一些人之间的关系,例如:1号,2号是朋友,1号,3号是朋友,那么他们都属于一个帮派。现在的问题是,在这些关系下,有多少帮派,没人属于哪个帮派。

分析:简单来说,n个结点的图,给定m条有向边,问你有多少个连通分量,每个结点属于哪个分量。

这里就用并查集,将n个结点划分为不相交的集合,然后用一个集合中的某个结点来代表所在集合。

并查集的初始化

定义数组s[i]是以节点i为元素的并查集,在开始还没有处理朋友与朋友之间的关系时,每个人都属于一个独立的集,并且以元素i的值表示它的集合s[i]。

在这里插入图片描述

并查集的合并

例如加入第一个朋友关系(1,2),就是将朋友1所在的“帮派”与朋友2所在的“帮派”进行合并。递归查找到结点1所在的集为1,结点2所在的集为2,将结点1的集1改成集2(改哪个没什么实际影响)。

在这里插入图片描述

加入第二个朋友关系(1,3),递归查找到结点1所在的集为2,结点2的集为2,结点3所在的集为3,将元素2的集2合并到集3,此时结点1,2,3属于一个集。

在这里插入图片描述
这里的合并并不是一个很好的合并,因为大家可以发现一个集合只有3个元素,但是链的长度已经达到了3,树退化成了长链,查找的复杂度会很高。

并查集的查找

在前面的操作中就已经需要查找操作了:查找元素的集是一个递归的过程,例如我们查找上面的并查集中结点1的所在集。

此时s[1]不等于1,说明1不是结点1的所在集,对s[1]=2进行查找,s[s[1]]=s[2]=3,此时s[2]不等于2,继续向下查找,s[3]=3,说明3是1的所在集。

可以发现像上面这种退化成长链的情况会影响查找的速度,所以在实际操作的过程中还需要很多的优化

统计有多少个集

如果s[i]=i,说明这是一个根结点,根结点的数量即是集的数量。

看一下并查集在实际问题中的运用。

hdu 1213 “How Many Tables”

有n个人一起吃饭,有些人互相认识,认识的人想坐一桌,例如A认识B,B认识C,那么A,B,C就会坐同一桌。

问需要给出多少张桌子。

分析:就是我们拿来做例子的帮派问题。

#include<iostream>
using namespace std;
int s[1050];
void init_set(int n){
   for (int i=1;i<=n;i++) s[i]=i;}//初始化并查集
int find_set(int x){
   return x==s[x]?x:find_set(s[x]);}//查找并查集,s[x]=x时,说明为集合的根节点
void union_set(int x,int y){
   x=find_set(x); y=find_set(y);//合并并查集,找到两个集的编号 
	if (x!=y) s[x]=s[y];//如果不是同一个集,将其中一个集代表元素的s值改成另一个集合的代表元素即可 
}
int main(){
   int t,n,m,x,y; cin>>t;
	while (t--){
   cin>>n>>m; init_set(n);//初始化并查集 
		for (int i=1;i<=m;i++){
   cin>>x>>y; union_set(x,y);}//合并两个朋友的所在"帮派" 
		int ans=0; for (int i=1;i<=n;i++) if (s[i]==i) ans++;//统计集的个数
		cout<<ans<<endl; 
	} return 0;
} 

并查集操作的优化

以上都是最简单的操作,在实际的使用中往往性能比较差,下面介绍一些优化的方法。

合并的优化

给集合对应的树添加高度的属性,将高度较小的树加入到高度较大的树的根节点下。

int height[maxn];//表示集i对应树的高度
void init_set(int n){
   //初始化并查集,树的高度初始化为0 
	for (int i=1;i<=n;i++) {
   s[i]=i; height[i]=0;}
}//并查集合并 
void union_set(int x,int y){
   x=find_set(x); y=find_set(y);
	if (height[x]==height[y]){
   height[x]=height[x]+1; s[y]=x;}//两个树一样高,无法用一个树覆盖另一个树,合并后树的高度+1
	else if (height[x]<height[y]) s[x]=y;//y的树比较高,用y的树来覆盖x树,高度保持为y树的高度 
	else s[y]=x;//x的树比较高,用x树来覆盖y树,高度保持为x树的高度 
} 
查询的优化

这个优化我们在Kruskal算法中就见到过了,就是在查询搜索的过程中,将路径上的结点的前一个结点全部改成根节点。

这种优化方式降低了树的深度,提高了下次查询的效率。
在这里插入图片描述

代码的话比较简单,加一个赋值即可:

//并查集查询 
int find_set(int x){
   return s[x]==x?x:s[x]=find_set(s[x]);}

整体的模板如下:

int height[maxn];//表示集i对应树的高度
void init_set(int n){
   //初始化并查集,树的高度初始化为0 
	for (int i=1;i<=n;i++) {
   s[i]=i; height[i]=0;}
}//并查集查询 
int find_set(int x){
   return s[x]==x?x:s[x]=find_set(s[x]);} 
//并查集合并 
void union_set(int x,int y){
   x=find_set(x); y=find_set(y);
	if (height[x]==height[y]){
   height[x]=height[x]+1; s[y]=x;}//两个树一样高,无法用一个树覆盖另一个树,合并后树的高度+1
	else if (height[x]<height[y]) s[x]=y;//y的树比较高,用y的树来覆盖x树,高度保持为y树的高度 
	else s[y]=x;//x的树比较高,用x树来覆盖y树,高度保持为x树的高度 
}

习题

poj 2524 “Ubiquitous Religious”

直接看分析

分析:没有看题,看了一下数据感觉就是前面说的问题,改了一下直接过了,没啥好说的。

#include<iostream>
using namespace std; 
int height[50010],s[50010];//表示集i对应树的高度
void init_set(int n){
   //初始化并查集,树的高度初始化为0 
	for (int i=1;i<=n;i++) {
   s[i]=i; height[i]=0;}
}//并查集查询 
int find_set(int x){
   return s[x]==x?x:s[x]=find_set(s[x]);} 
//并查集合并 
void union_set(int 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值