数据结构——并查集

No.1 引入

Day 3,Day 4学了有关并查集的知识,为加强记忆,且有感而发。


No.2 概念

简易

并查集,是一种数据结构,可以用来处理集合的合并查询问题。

完整

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


No.3 操作

操作主要为三类。

1)建并查集。

并查集由于是树形结构,所以保存的方式是使用father数组来存储。

建立时,由于每一个单位所处并查集都为它本身,所以它的father就是它本身。

void MakeSet(int n){
	for(int i=1;i<=n;i++){
		fa[i]=i;
	}
}

 2) 查找元素所处树根。

建并查集时,已初始化father

如果此结点的father是它本身,也就是它没有前继,那此结点即是树根。

因是查找结点,所以有返回值,为int

    如图,如要查询结点1所在并查集的根father往上爬,及结点2。因结点2的father就是它本身,就找到了并查集的根。

int FindSet(int a){
	if(fa[a]==a){
		return a;
	}
	else{
		return FindSet(fa[a]);
	}
}

3) 合并两集合。

已知两集合中任意结点,要合并两结点所处集合,那就是说要把一个集合接在另一集合下。

因要接整个集合,那就得找到集合的代表,即是树根。把一树根接在另一树根下,就可完成集合合并。

还需注意,如两结点已处于同一并查集中,那就不用合并了,直接跳过。

void UnionSet(int a,int b){
	int l=FindSet(a),r=FindSet(b);
	if(l==r){
		return;
	}
	fa[l]=r;
}

No.4 优化

1)路径压缩

如果不对并查集进行优化,那么它就是一棵普通的树,查询所用时间复杂度很高。

如右图,如果我们不进行任何优化,从节点2到节点4就要递归3次

于是,就有了路径压缩

对于每次查询,我们都可以让这个节点直接连在根节点上。下一次查询,就可以直接查询到根节点。大大缩减了时间复杂度。

int FindSet(int a){
	if(fa[a]==a){
		return a;
	}
	else{
		return fa[a]=FindSet(fa[a]);
	}
}

2)按秩合并(启发式合并)

有两个并查集需合并,如果把深度深的接在深度浅的并查集后,那会大大加深并查集的深度。

如图,把节点7的并查集接在8后,会增加并查集的深度,但是,把节点8的并查集接在7后,就不会有此顾虑。

 所以,它启发了我们,应该把深度低的树往深度高的树上合并,就不会担心此问题。

可以用数组deep[i]来存并查集深度,在合并时比较就可以了。

void UnionSet(int a,int b){
	int l=FindSet(a),r=FindSet(b);
	if(l==r){
		return;
	}
	else if(de[l]<=de[r]){
		fa[l]=r;
	}
	else{
		fa[r]=l;
	}
	if(de[l]==de[r]){
		de[r]++;
	}
}

No.5 特殊类并查集

1)带权并查集(边带权并查集)

有时,题目会要求我们求链并查集的节点间隔,如此。但是代码又用了路径压缩,破坏了并查集原有的形态,这是,就要引出带权并查集。

顾名思义,此并查集中每一条边都带有值,我们称权值。

在每次查找路径压缩中,我们都借此更新权值,这样就不用担心此问题。

如左图,现在每边的权值都为1,但压缩到右图时,每边权值就分别成了1,2,3。

当我们求如3,2节点中间节点数时,就可直接3-1-1=2了。

                                                              

2)种类并查集(扩展域并查集)

有时候并查集要维护一种特殊关系:敌人的敌人是朋友

这时,再用一般的并查集就不合适了。所以我们引入了种类并查集。

在建并查集时,我们可以多建1倍或2倍并查集,来分别保存如题朋友关系和敌人关系。在查找关系时,忽略多建的点,这样就得到了我们想要的关系。


 No.6 典型例题

1)格子游戏

题目描述

Alice和Bob玩了一个古老的游戏:首先画一个n × n的点阵(下图n = 3)

接着,他们两个轮流在相邻的点之间画上红边和蓝边:

直到围成一个封闭的圈(面积不必为1)为止,“封圈”的那个人就是赢家。因为棋盘实在是太大了(n ≤ 200),他们的游戏实在是太长了!他们甚至在游戏中都不知道谁赢得了游戏。于是请你写一个程序,帮助他们计算他们是否结束了游戏?

输入格式

输入数据第一行为两个整数n和m。m表示一共画了m条线。以后m行,每行首先有两个数字(x, y),代表了画线的起点坐标,接着用空格隔开一个字符,假如字符是"D ",则是向下连一条边,如果是"R "就是向右连一条边。输入数据不会有重复的边且保证正确。

输出格式

输出一行:在第几步的时候结束。假如m步之后也没有结束,则输出一行“draw”。

样例

样例输入

3 5
1 1 D
1 1 R
1 2 D
2 1 R
2 2 D

样例输出

4

思路点拨:

由圈想到并查集,可把坐标放进并查集。

先判断两点是否在一个并查集中,如是,即已创建出一个圈,如否,把连成线的两点坐标放入并查集,继续下一步操作。

核心代码:

if(k=='D'){//向下画一笔
	z=x+1;
	int a=x*n+y;
	int b=z*n+y;//确定两点坐标
	if(FindSet(a)==FindSet(b)){//判断两点是否处于同一并查集。
		printf("%d",i);
		return 0;
	}
	else{
		UnionSet(a,b);//否,则将两点搞入并查集
	}
}
else{//向右画一笔
	z=y+1;
	int a=x*n+y;
	int b=x*n+z;
	if(FindSet(a)==FindSet(b)){//判断两点是否处于同一并查集。
		printf("%d",i);
		return 0;
	}
	else{
		UnionSet(a,b);//同上
	}
}

 2)老旧的桥

题目描述

现在有个岛屿和座桥。

第座桥双向连接着第和第个岛屿。

刚开始的时候,我们可以用其中的一些桥在任何两个岛之间旅行。

然而,经调查显示,这些桥都将会因老旧而倒塌,而且倒塌的顺序为从第1座桥到第M座桥。

由于剩下的桥不能够使第个岛和第个岛互相到达,这会使岛屿对变得很不方便。

对于每一个,找出在第i座桥倒塌之后,这样不方便的岛屿对有多少?

输入格式

第一行有两个正整数和,分别表示岛屿的数量和桥的数量。

接下来有行,每一行有两个整数和,分别表示第i座桥连接的两个岛屿的编号。

输出格式

输出有行,每一行输出一个整数,表示第座桥倒塌之后不方便到达的岛屿对的数量,结果可能会超出位的大小。

样例

样例输入

4 5
1 2
3 4
1 3
2 3
1 4

样例输出

0
0
4
5
6

思路点拨:

倒着枚举每座桥,方便并查集建集和计算。

乘法原理,如两集合不通,则每集合中选1点都不能沟通。所以相乘。

核心代码:

void UnionSet(int x,int y,int z){
	int p=FindSet(x),q=FindSet(y);
	if(p==q){//如根相等,即它们在一集合,就继承上一ans
		ans[z]=ans[z+1];
		return;
	}
	else{
		fa[q]=p;
		ans[z]=ans[z+1]-si[q]*si[p];//乘法原理
		si[p]+=si[q];
	}
}

 No.7 感想

前两天Day 1,Day 2状态不好,但Day 3,Day 4有所回升。

写博客既是总结知识,又是对自己的提醒和督促。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值