首页 > 其他分享 >并查集学习笔记

并查集学习笔记

时间:2023-07-09 23:47:15浏览次数:39  
标签:元素 return int 查集 笔记 学习 fa 集合 find

什么是并查集

顾名思义,并查集有两个最主要的作用:合并集合和查询某两个元素是不是在同一个集合内。
或者说:

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。 ——百度百科

怎么实现并查集

思路

如果两个元素在一个集合里,那么我们可以把这一个集合理解为一个家族,一个元素是另外元素的父亲,如果集合中出现更多元素,那么这个家族会更加庞大,但无论如何,都会有一个最远公共祖先(我瞎命名的,方便描述),由于集合本身具有无序性,所以说这个最大的祖先可以是集合中的任何一个元素,所有以这个元素为祖宗的数就是集合中的数,现在假设在集合 \(A\) 中有这样的祖先 \(a\) ,如果想要将集合 \(A\) 与集合 \(B\) 合并,那么设集合 \(B\) 中的最远公共祖先为 \(b\) ,如果将 \(a\) 的祖先改成 \(b\) ,那么集合 \(A\) 、 \(B\) 中所有的元素都有一个共同的祖先,所以此时他们就被合并为同一个集合了。
这样子的话,如果想要查找 \(a\) 和 \(b\) 是否在一个集合中,只需要看他们的祖先是否相同了

ps:

有没有发现这样的思路与树很像?所以说并查集也是一种树形的数据结构,合并就是将森林变成树,而查询则是查询两个子节点是否在同一棵树上

实现

模版题:洛谷P3367
首先是一个必不可少的数组: \(fa_i\) 即一个元素的父亲(稍后会有优化),一开始,每个元素自己为一个集合,所以说初始化,令元素的父亲为自己:

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

接下来,如何寻找祖宗呢?这里采用递归的方式,如果一个元素的父亲是它本身的话,那么他就是这个集合中的最远公共祖先,所以可以写出 \(find()\) :

int find(int x){
	if(fa[x]==x) return x;
	return find(fa[x]);
}

合并集合也就是让一个集合的祖宗的父亲为另一个集合的祖宗:

fa[find(y)]=find(z);

查询操作:

if(find(y)==find(z))

完整代码:

#include<stdio.h>
int fa[10005];
int find(int x){
	if(fa[x]==x) return x;
	return find(fa[x]);
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		fa[i]=i;
	}
	for(int i=1;i<=m;i++){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		if(x==1){
			fa[find(y)]=find(z);
		}
		else{
			if(find(y)==find(z)){
				printf("Y\n");
			}
			else{
				printf("N\n");
			}
		}
	} 
	return 0;
} 

这时候我们会发现一个很尴尬的问题——超时了。

优化

考虑 \(fa_i\) 可以直接保存它的祖先而非父亲,这样递归函数可以省去很多时间,也就是说,我们可以将原来的树转化为一个除了根节点就是叶子的树,于是修改 \(find()\) :

int find(int x){
	if(fa[x]==x) return x;
	return fa[x]=find(fa[x]);
}

得到 \(ACcode\) :

#include<stdio.h>
int fa[10005];
int find(int x){
	if(fa[x]==x) return x;
	return fa[x]=find(fa[x]);
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		fa[i]=i;
	}
	for(int i=1;i<=m;i++){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		if(x==1){
			fa[find(y)]=find(z);
		}
		else{
			if(find(y)==find(z)){
				printf("Y\n");
			}
			else{
				printf("N\n");
			}
		}
	} 
	return 0;
} 

并查集的应用

直接套用:

洛谷P1536
这题太过于板了,不给过程了。

最小生成树——— \(Kruskal\) 算法

什么是最小生成树

一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。 ——百度百科

或者说,在有 \(n\) 个节点的有 \(k\) 条边的图里保留 \(n-1\) 条边,使边的长度总和最小

什么是 \(Kruskal\)

一种用来实现最小生成树的算法
其原理类似贪心,从小到大地顺次排列 \(k\) 条边,在不出现自环的情况下顺次加入边,直至加入 \(n-1\) 条边

\(Kruskal\) 与并查集的联系

并查集起到的作用是:查自环
如果两个点已经属于一个集合(联通)时,那么再出现一条链接两点的边,必定形成自环。

代码

#include<stdio.h>
#include<algorithm>
using namespace std;
int n,m;
int fa[5005];
int ans,cnt;
struct e{
	int x,y,c;
}edge[200005];
bool cmp(e a,e b){
	return a.c<b.c;
}
int find(int x){
	if(fa[x]==x) return fa[x];
	else return fa[x]=find(fa[x]);
}
int main(){	
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&edge[i].x,&edge[i].y,&edge[i].c);
	}
	sort(edge+1,edge+1+m,cmp);
	for(int i=1;i<=n;i++){
		fa[i]=i;
	}
	for(int i=1;i<=m;i++){
		int k=find(edge[i].x);
		int p=find(edge[i].y);
		if(k==p){
			if(i==m){
				printf("orz");
				return 0;//无法实现
			}			
		}
		else{//查自环
			fa[k]=p;
			ans+=edge[i].c;
			cnt++;
			if(cnt==n-1){
				break;
			}			
		}
	}
	printf("%d",ans);
	return 0;
} 

技巧——反集

例题:洛谷P1892
如果 \(a\) 不在 \(A\) 集合中,那么我们可以理解为它在‘不在 \(A\) 集合’的集合中,那此时,我们可以把不在 \(A\) 集合中的元素全部都放到这个集合里去,我们可以理解为:这个集合就是 \(A\) 的反集。
在这道题中,如果 \(a\) 与 \(b\) 是敌人,那么我们把 \(a+n\) 与 \(b\) 合并,这就相当于将 \(b\) 加入与 \(a\) 为友的反集,这样子所有在反集里的元素都是朋友,也都是 \(a\) 的敌人,满足敌人的敌人是朋友的题意。
同理,如果 \(a\) 是 \(b\) 的朋友,那么将 \(a\) 与 \(b\) 合并,此时所有 \(b\) 的朋友也成为了 \(a\) 的朋友,这符合朋友的朋友是朋友的题意
所以代码如下:

#include<stdio.h>
#include<iostream>
using namespace std;
int n,m;
int fa[2005];
int find(int x){
	if(fa[x]==x) return fa[x];
	else return fa[x]=find(fa[x]);
}
int ans;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=2*n;i++){
		fa[i]=i;
	}
	for(int i=1;i<=m;i++){
		char a;
		int b,c;
		cin>>a;
		scanf("%d%d",&b,&c);
		if(a=='E'){
			fa[find(b+n)]=find(c);
			fa[find(c+n)]=find(b);
		}
		if(a=='F'){
			fa[find(b)]=fa[find(c)];
		}
	}
	for(int i=1;i<=n;i++){
		if(fa[i]==i)ans++;
	}
	printf("%d",ans);
	return 0;
}

结语

本文只讲述了很基础的有关于并查集的知识,作者也需要再学习,有任何不当请批评指正,谢谢。

标签:元素,return,int,查集,笔记,学习,fa,集合,find
From: https://www.cnblogs.com/nxhx/p/disjoint-set_basic.html

相关文章

  • python学习巩固一(基础语法)
    大学四年毕业,对于计算机还是一头雾水,现在准备去读研了,导师要求我好好掌握python,突然回想到我学python的时候曾注册过博客园,哈哈哈,找回密码后发现我账号竟然有三个粉丝,某些阅读量还挺高的,感谢感谢。为了督促自己这次能认认真真再好好学习python,我又开始弄我的博客园了,现在从零开始,......
  • 学习总结:《代码中的软件工程》
    在学习过程中,我对《代码中的软件工程》这本书有了一些深入的理解,并结合本课程的学习内容,我想就一些亮点和个人见解进行总结。通过学习,可以系统掌握软件工程这门实践与理论相结合的学科;对于复习系统知识,进阶理论来说大有裨益,本书的框架如下,推荐大家参考和阅读:•【实践为主】工欲......
  • Hash 学习笔记与总结
    Hash算法学习笔记与总结目录Hash字符串Hash信息学奥赛一本通AcWing模板模板题题目大意CODEHash表拉链法开放寻址法模板题题目大意CODEHash哈希算法是通过一个哈希函数H,将一种数据(包活字符串、较大的数等)转化为能够用变量表示或是直接就可作为数组下标的数,道过哈希函数......
  • 高等数学笔记
    第一章函数与极限第一节映射与函数映射函数......
  • 学习博客链接
    Eva-J的博客:https://www.cnblogs.com/Eva-J/94007的博客:https://www.cnblogs.com/hswangnux/category/1274022.html隔壁老王的博客:https://www.cnblogs.com/wangfengming/隔壁老王MySQL博客:https://www.cnblogs.com/wangfengming/category/1118634.html隔壁老王MySQL正则表达......
  • 【《C++ Primer 第四版》读书笔记】4.2.5-指针和const限定符
    1.指向const对象的指针1.1表现形式constdouble*ptr,constvoid*ptr1.2如何理解无法通过ptr这个指针变量去修改所指向内存区域的值,但是ptr这种指针变量可以重复赋值,指向不同的内存地址注意ptr这个指针变量赋值时,既可以赋值为const类型变量(书中所说的const对象)的地址,也......
  • day2c++学习
    学习day2C++函数分文件编写(VScode2021配置教程)_spiritLHL的博客-CSDN博客55函数-函数的分文件编写_哔哩哔哩_bilibili!运行还是有中文乱码st1:ctrl+shift+p输出createc++projectst2:在include里建新文件swap.h,里面写头文件和函数声明st3:在src里建新文件swap.cpp......
  • 平面图学习笔记--zhengjun
    要点不多,记一下即可。\(G\)的对偶图记为\(G^*\)。\(G^*\)为连通图,若\(G\)联通,则\(G^{*}{^*}=G\)\(G^*\)中的简单环对应着\(G\)中的极小割,(简单对应极小),利用该性质,可以把平面图上的最小割问题转化为对偶图上的最短路问题平面图欧拉公式:\(V-E+F-C=1\),点数-边数+面......
  • Docker学习路线1:介绍
    Docker是什么?Docker是一个开源平台,通过将应用程序隔离到轻量级、可移植的容器中,自动化应用程序的部署、扩展和管理。容器是独立的可执行单元,封装了运行应用程序所需的所有必要依赖项、库和配置文件,可以在各种环境中稳定地运行。什么是容器?容器是一种轻量级、可移植和隔离的软件......
  • 【学习笔记】李超线段树
    维护一次函数以模板题为例。使用线段树维护线段,每个节点维护的都是完全覆盖这个区间的线段。考虑当前节点已经有线段\(f\),现在加入线段\(g\)。暴力想法是暴力递归每个子区间,把更优的保留,注意到\(f,g\)最多一个交点,因此也最多一侧的子区间需要暴力递归。具体流程如下:先......