多图生动理解并查集&最小生成树(附核心代码模板)

多图生动理解并查集&最小生成树(附核心代码模板)

相关知识

并查集是什么?

​​  并查集指的对于集合及其元素两类“操作”,即“并”和“查”。简单来说,“并”指的是不同集合合并操作,“查”指的是查看元素具体属于哪一个集合

如何进行集合初始化?

并查集涉及集合,那么集合如何初始化呢?是初始化为空集,还是本身,抑或其它?
​​  开始时,每一个元素的集合都是未知的,我们规定:一开始的时候每一个元素的所在集合都是在以自己为名的集合。因此,集合初始化长这样:for(i=1;i<=n;i++)sets[i]=i;
​​  其中,元素从1~n,sets[i]代表第i个元素所在的集合名。

合并后的新集合名是什么?

​​  一开始1在集合1,3在集合3,5在集合5,现在要把135合并到一个集合中,那么这个集合是集合几?
​​  类似于集合初始化,我们继续规定,新集合中的最小值为合并后的新集合名。

并查集和树有什么关系?

​  本质上来说,一颗树结构就是一类集合,不过是一类有联系的集合
​  树的命名规则还是同集合,以树中最小的节点作为树名,作为祖先。同集合中以最小的元素作为祖先,如下图所示,树1本质上也是集合5={1,2,3,4},树5本质上也是集合5={5,6,7}。

​  并查集,一个操作是“查”。对于任意一个元素i,我们既可以查它的父亲(Sets[i])也可以查它的祖先([Find(i)),如下图所示(sets是我们定义的集合,sets[i]的值代表第i个元素的父节点信息;find是我们定义的一个查找函数,find(i)代表的值第i个元素的祖先节点信息,如图下代码所示):

“查”的代码:

//1.并查集中查元素i的父亲
// sets[i]即可
//2.并查集中的查祖先(先定义查函数后定义并函数,因为并的过程中要用到查函数)
int find(int x){ //并查集中的查
   int r=x; //不能改变x的值,因此定义一个变量r来等价x
   while(sets[r]!=r){ //通过不断地访问父亲的父亲,找到祖先
           r=sets[r];
   		}
   return r; //返回祖先的值
}

​  “并”的操作,本质上就是将两颗单独的树进行连接,具体怎么连接,由连接节点决定,如下图所示,就是连接节点3和节点5:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4XnaMPK4-1646318565269)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220303094711089.png)]
​  以下是并查集中并的代码:

// 并查集中的并
void merge(int a,int b){ //并查集中的并,只需要改变,不需要返回值,因此就用void即可
   int fa=find(a),fb=find(b); //找元素a,元素b的祖先,存于fa,fb中
   if(fa!=fb) sets[fb]=fa; //若祖先不同,就将fb的祖先改成fa,即可实现合并
}

以上,就是“并查集”在“树”中的应用的形象阐述。

什么是最小生成树?

​  假设有如下的一个带权图

​  在这个图中,总共有6个节点,10条带权值的边。最小生成树指的具有最小边权值和的全节点联通树。
​  举个例子,我们将这6个节点理解为6座城市,这10条边理解为相应的城市与城市之间可以搭建的桥,边上的权值对应建桥的经济费用。此时的最小生成树,指的就是花最小经济费用下实现6座城市联通的桥梁搭建形式

如何在图上选边构成最小生成树?


​  克鲁斯卡尔算法是解决最小生成树问题的一种常见方法。
​  根据贪心策略,先用结构体数组存储所有的边信息(包括边的两个节点,以及边上的权值),例如“1 4 5”代表节点1和节点4之间的距离是5。然后再根据节点间距的大小进行升序排序。然后进入条件选边模式,从1~N(总边数)依次选边,如果满足不会构成回路==(待连接的两个点的祖先不相同,就不会构成回路;反之依然)==,就选择这条边,直至到达顶点数-1条边,终止选边。此时构成的树,就是最小生成树。
​  以下就是利用克鲁斯卡尔算法求解最小生成树问题的核心代码示例(有用到前面讲的find查函数):

struct node{    //将图中的边信息存储到edge数组中
   int u,v,w;   //两个节点的信息以及两个节点之间的距离
}edge[105*105]; //edge存储“边”信息,数组规模可修改
bool cmp(node a,node b){ //定义排序规则:按照权重从小到大排序
   return a.w<b.w;
}
   int cnt=0,sum=0; //cnt表示选中的边数,sum表示目前的权值和
   //“克鲁斯卡尔算法”的应用,n代表边数,m代表节点数
   sort(edge+1,edge+n+1,cmp);//将边数组的信息按照距离进行升序排序,
   for(int i=1;i<=n;++i){ //遍历所有边,进行边选择
       if(cnt==m-1)break;
       int c=find(edge[i].u),d=find(edge[i].v); //将桥的两个节点的祖先信息存储在c和d中
       if(c!=d) sets[c]=d,cnt++,sum+=edge[i].w; //环路判断,不是同祖先,就可以选入,同时更新cnt和sum的信息
   }

并查集的核心代码模板

// 1.并查集中的集合定义+最小生成树的“边”信息定义(边信息包括“边的两个节点”以及“边的权重”)
//只能用sets而不能用set,因为set是c++中的一种容器,用set就和它冲突了,会报错
int sets[1005]; //长度不一定是1005,并查集中集合的代码一定要在"最前面定义",作为“全局变量”,更合适
struct node{    //将图中的边信息存储到edge数组中
   int u,v,w;   //两个节点的信息以及两个节点之间的距离
}edge[105*105]; //edge存储“边”信息,数组规模可修改
bool cmp(node a,node b){ //定义排序规则:按照权重从小到大排序
   return a.w<b.w;
}

// 2.并查集中的查(先定义查函数后定义并函数,因为并的过程中要用到查函数)
int find(int x){ //并查集中的查
   int r=x; //不能改变x的值,因此定义一个变量r来等价x
   while(sets[r]!=r){ //通过不断地访问父亲的父亲,找到祖先
           r=sets[r];
   		}
   return r; //返回祖先的值
}

// 3.并查集中的并
void merge(int a,int b){ //并查集中的并,只需要改变,不需要返回值,因此就用void即可
   int fa=find(a),fb=find(b); //找元素a,元素b的父亲,存于fa,fb中
   if(fa!=fb) sets[fa]=fb; //老大不是一个人,说明不在一个集合里,让fa的领导改成fb,那么就只有一个老大fb了
}

// 4.并查集中的集合初始化,均自己先是自己的祖先
   //以下,n代表边数,m代表节点数,节点编号默认从1到m
for(int i=1;i<=m;i++)sets[i]=i;

// 5.最小生成树权值和计算(考虑了无法构成最小生成树的情况,可以根据题境灵活修改)
   int cnt=0,sum=0; //cnt表示选中的边数,sum表示目前的权值和
   //“克鲁斯卡尔算法”的应用,n代表边数,m代表节点数
   sort(edge+1,edge+n+1,cmp);//将边数组的信息按照距离进行升序排序,
   for(int i=1;i<=n;++i){ //遍历所有边,进行边选择
       if(cnt==m-1)break;
       if(find(edge[i].u)!=find(edge[i].v)) cnt++,sum+=edge[i].w;//不构成环路,就选择
       merge(edge[i].u,edge[i].v); //连接两个节点
   }
//以上模板经过题境适当修改后即可用

并查集可以解决哪些问题?

主要包括三类问题:
​  ①图的连通性问题:是不是各个节点之间可以相互到达?至少还差几条边才能相互到达?
​​  图联通之后就只能有一个祖先,那么只需要判断,距离一个祖先还差多少,即可求出还差多少边就能相互到达。例如,现在4个祖先,那么就还差4-1=3条边即可实现联通。
​​  利用并查集中的“查”,查祖先是自己的元素个数,然后减1,即为距离连通还差的边数。
​  ②最小生成树问题:给我们一张图,怎么在图中选边构造树,才能让生成树的各边权值和最小?如,权值是钱,就是问怎么搭桥最省钱;权值是里程,就是问怎么搭桥里程最短?
​​  利用克鲁斯卡尔算法模板(用到了并查集中的并和查)即可,最后的sum值就是最小生成树的权值。
​  ③图类的判断问题:规定一类图,比如有没有环路,怎么判断输入的图是不是符合定义规则的图?
​​  在每一次连接两个元素的时候,查看这两个元素是否同祖先。如果同祖先,再连接一定会形成环路。

相关题目

图的连通性问题

畅通工程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YSZ5Lksp-1646318565271)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220303210114460.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ERGarVye-1646318565271)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220303210126009.png)]
参考思路
​  查祖先是自己的元素个数,然后减1,即为距离连通还差的道路数目。
参考代码

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"
int sets[1002]; 
int find(int x){
	int r=x;
	while(sets[r]!=r){
		r=sets[r];
	}
	return r;
}
void merge(int a,int b){
	int fa=find(a),fb=find(b);
	if(fa!=fb) sets[fa]=fb;
}
int main(){
	ios::sync_with_stdio(false);
	std::cin.tie(0);
	int a,b,cnt,city,qiao;
	while(cin>>city&&city){
//		if(city==0)return 0;
		cin>>qiao;
		for(int i=1;i<=city;i++)sets[i]=i;
		for(int i=1;i<=qiao;i++){
			cin>>a>>b;
			merge(a,b);
		}
		cnt=0;
		for(int i=1;i<=city;i++){
			if(sets[i]==i)cnt++;
		}
		cout<<cnt-1<<endl;
	}
	return 0;
}

最小生成树问题

1、还是畅通工程(基本版最小生成树)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KaOIrMMm-1646318565272)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220303211612211.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ymug9Me-1646318565272)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220303211621094.png)]参考思路
​  遍历每条边,通过克鲁斯卡尔算法选边连接,求得最终的最小生成树的权值和。
参考代码

#include<bits/stdc++.h>
using namespace std;
int n,vis[100];
struct ct{
   int c1,c2,d;
};
bool cmp(ct a,ct b){ //按照距离从小到大排序
   return a.d<b.d;
}
int find(int x){
   int r=x;
   while(vis[r]!=r){
       r=vis[r];
   }
   return r;
}
void merge(int a,int b){
   int fa=find(a),fb=find(b);
   if(fa!=fb) vis[fa]=fb;
}
int main() {
   ios::sync_with_stdio(false);
   ct c[4852];
   while(cin>>n&&n){ //输入城市数量
       int m=n*(n-1)/2;
       int sum=0;
       for(int i=1;i<=n;i++){ //初始化并查集
           vis[i]=i;
       }
       for(int i=1;i<=m;i++){
           cin>>c[i].c1>>c[i].c2>>c[i].d;    
       }
       sort(c+1,c+m+1,cmp); //将输入的数据按距离从小到大排序
       for(int i=1,k=0;i<=m;i++){
           if(find(c[i].c1)!=find(c[i].c2)) k++,sum+=c[i].d; //不会串联,就加上距离于sum,增加一次相连次数k
           merge(c[i].c1,c[i].c2); //然后再相连两座城市
           if(k==n-1) break; //如果连了n-1条桥,说明足够了,可以跳出for循环
       }
       cout<<sum<<"\n"; //减少代码运行时间
   }
   return 0;
}

2、畅通工程-2007(不一定能畅通的考虑)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P3NH4S2d-1646318565273)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220303214457935.png)]
参考思路
​  大体思路同上题,克鲁斯卡尔算法循环出来后,看选边数有没有达到m-1,没有就输出?,有就正常输出即可。
参考代码

#include<bits/stdc++.h>
using namespace std;
int n,m,vis[105];
int find(int x){
   return vis[x]==x?x:vis[x]=find(vis[x]);
}
void merge(int a,int b){
   int fa=find(a),fb=find(b);
   if(fa!=fb) vis[fa]=fb;
}
struct node{
   int u,v,w;
}edge[105*105];
bool cmp(node a,node b){
   return a.w<b.w;
}
void solve(){
   for(int i=1;i<=m;++i) vis[i]=i;
   for(int i=1;i<=n;++i){
       int u,v,w;
       cin>>u>>v>>w;
       edge[i]={u,v,w};
   }
   sort(edge+1,edge+n+1,cmp);
   int cnt=0,sum=0;
   for(int i=1;i<=n;++i){
       if(cnt==m-1)break;
       if(find(edge[i].u)!=find(edge[i].v))
           merge(edge[i].u,edge[i].v),cnt++,sum+=edge[i].w;
   }
   if(cnt==m-1) cout<<sum<<'\n'; //如果不生成环路的情况下能搭建的桥数能够等于n-1,那么就能输出数值,否则输出问号
   else cout<<"?\n";
}
int main(){
   ios::sync_with_stdio(0);
   while(cin>>n>>m&&n) solve(); //题目意思就是,如果n=0,还是要把m输入才行
}

3、继续畅通工程(已经有连接的考虑)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2wJzb387-1646318565274)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220303214903584.png)]
参考思路

​  基本思路仍如上,只不过需要对连好的和没有连好的进行分类讨论——已经连好的,我们就用merge把它们连上,没连好的,我们就先把它们存在edge数组里面,后续用克鲁斯卡尔算法进行筛选即可。

参考代码

#include<bits/stdc++.h>
using namespace std;
int n,vis[105];
int find(int x){
   return vis[x]==x?x:vis[x]=find(vis[x]);
}
void merge(int a,int b){
   int fa=find(a),fb=find(b);
   if(fa!=fb) vis[fa]=fb;
}
struct node{
   int u,v,w;
}edge[105*105];
int cmp(node a,node b){
   return a.w<b.w;
}
void solve(){
   for(int i=1;i<=n;++i) vis[i]=i;
   int m=n*(n-1)/2,num=0,sum=0; //m代表可连接总数
   for(int i=1;i<=m;++i){ //for循环存储可连接的长度和状态w表示长度,op表示状态
       int u,v,w,op;
       cin>>u>>v>>w>>op;
       if(op){ //如果是已经连接好的,就建立连接
           merge(u,v);
       }
       else edge[++num]={u,v,w}; //没连接的先不直接连,存在边信息里,到时候用克鲁斯卡尔算法筛选一下   
   }
   sort(edge+1,edge+num+1,cmp); //根据成本从小到大排序
   for(int i=1;i<=num;++i){
       int c=find(edge[i].u),d=find(edge[i].v);
       if(c!=d) vis[c]=d,sum+=edge[i].w; 
   }
   cout<<sum<<'\n';
}
int main(){
   ios::sync_with_stdio(0);
   while(cin>>n&&n) solve(); //输入城镇数n
}

图类的判断问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7E5tiDtg-1646318565274)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220303220706208.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QLipRuHL-1646318565276)(C:\Users\49593\AppData\Roaming\Typora\typora-user-images\image-20220303220713912.png)]
参考思路

初始化

​  初始化的时候要注意,由于这题并不是1~m,因此我们先把所有元素的祖先都定义为0,导入数据时,如果祖先为0,那么就让它们初始化为自己的祖先,即用到啥元素初始化啥元素。

迷宫判断

​  先判断是不是一个迷宫,迷宫的话各节点只能有一个祖先,例如下图有3个祖先的图,就连迷宫都算不上。
​  再者,题中说不能有回头路,那么就在每次输入两个节点的时候看是否为同祖先,同祖先相连必然构成环路,也就是环路,如下图所示,2和3具有相同的祖先1,如果相连,就会构成回路

参考代码

#include<stdio.h>
#include<string.h>
#define N 100005 //大数值用常量运行更快,效率更高
int sets[N]={0}; //由于此时数据并不是数组数据全部都初始化为0
int find(int x)
{
   //return x==sets[x]?x:sets[x]=find(sets[x]);
   while(x!=sets[x])
   x=sets[x];
   return x;
}
void merge(int x,int y)
{
   int fy,fx;
   fy=find(y);
   fx=find(x);
   if(fy!=fx)
   {
       sets[fy]=fx;
   }
}
int main()
{
   int k,i,a,b,flag;
   int sum;
   k=1;
   while(1)
   {
       flag=0;
       while(scanf("%d %d",&a,&b)!=EOF&&a&&b) //当输入0 0的时候跳出此循环,进入下一个循环判断
       {

           if(a==-1||b==-1) //输入某个数值全部都结束,最快的方式是直接return 0;
           return 0;
           if(sets[a]==0) sets[a]=a; //如果某房间没有老大,那么我们就让他做老大
           if(sets[b]==0) sets[b]=b; 
           if(find(a)==find(b)) flag=1; //如果同祖先,就会导致环路,用flag=1记录这种情况 
		   merge(a,b); //连接该两点 
       }
       for(i=1,sum=0;i<=N;i++) //查找祖先数 
       {
           if(sets[i]==i) sum++;
           sets[i]=0;
       }
       if(sum>1||flag==1) //多祖先\环路任意一种情况存在,都不是小希的迷宫 
       printf("No\n");
       else printf("Yes\n");
   }
   return 0;
}

参考

《2021杭电ACM-LCY算法培训入门班》
  有任何问题,可以在评论区留言~
    祝点赞的各位码力飞进!!!谢谢~

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值