【c++算法篇】--图论之克鲁斯卡尔


在这里插入图片描述

下面让我们来具体介绍一下

概念

克鲁斯卡尔是一种求连通图的最小生成树的算法(用最少的路线连接所有点),它的时间复杂度为O(nloge)n为边数

原理

通过以结构体中的权值为排序对象来排序结构体,通过getf()函数来寻找有没有共同联通点,有的话就跳过,没有的话就进行加边操作并且记录答案,详细内容在下文实现方式中会讲到。
下面让我们用图的形式感受一下:
在这里插入图片描述
这里可以发现,1-2最小值为1,所以连接这条线,并且2的“爹”=1;
在这里插入图片描述
找完最小的边找第二条边,这时候1和4也就连通了
在这里插入图片描述
连接这条边后,他们的爹都统一了,所以就全部连通了
在这里插入图片描述

所以现在所有点的爹都有同一个值,那么他们就联通了
在算法中会通过寻找每一个值的爹来判断他们是否连通,如果连通那么就不管,如果没连通,就要进行加边操作,并且记录答案。

作用

通俗易懂的来说,就是用最小的权值连通图中所有点

实现方式

下面来看一道题
P3366 【模板】最小生成树
题目描述
如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 orz。

输入格式
第一行包含两个整数 N,M表示该图共有 N 个结点和 M 条无向边。

接下来 M 行每行包含三个整数 X_i,Y_i,Z_i
,表示有一条长度为 Z_i​
的无向边连接结点 X_i,Y_i
输出格式
如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出 orz。

输入输出样例
输入 #1复制
4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3
输出 #1复制
7
说明/提示
数据规模:

对于 20%20% 的数据,N≤5,M≤20。

对于 40%40% 的数据,N≤50,M≤2500。

对于 70%70% 的数据N≤500,M≤10^4
对于 100%100% 的数据:1≤N≤5000,1≤M≤2×10^5,1≤Z ​≤10 ^4 请添加图片描述

样例解释:
所以最小生成树的总边权为 2+2+3=72+2+3=7。
这是一道经典的最小生成树模板题,具体细节让我们在代码中体现

#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
struct node{
	int u,v,w,next;

	bool operator < (const node &a)const{
		return w<a.w;//这里时以w为比较对象进行排序的意思,和下文sort一起用
	}

}e[1010101];
int m,n;
int f[1010101];
int getf(int x){
	return f[x]==x?x:f[x]=getf(f[x]);//这里就是上文所说,可以理解为“找爹”,具体解释在此题代码下方
}
inline int kruskal(){
	int val=0,cnt=0;
	sort(e+1,e+1+m);//无论如何记得排序
	for(int i=1;i<=m;i++){
		int u=e[i].u,v=e[i].v;//如果两个点没有连通那么就记录数据
		int xx=getf(u);
		int yy=getf(v);
		if(xx!=yy){
			cnt++,val+=e[i].w,f[xx]=yy;//边数加以,记录w,同步两点的“爹”
		}
		if(cnt==n-1) return val;
	}
	return -1;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		f[i]=i;
	for(int i=1;i<=m;i++){
		cin>>e[i].u>>e[i].v>>e[i].w;//因为这里是单项图,所以可以直接输入
	}
	int x=kruskal();
	if(x==-1) 
		cout<<"orz";
	else 
		cout<<x;
	return 0;
}


getf函数详解

getf(int x){
	return x==f[x]?x:f[x]=getf(f[x]);
}这里是说当点x等于找爹后的值,说明已经找到头了,
如果是这样就return x。如果不是这样就继续找爹,
所以f[x]要等于getf(f[x]),需要去以当前找到的爹为递归对象去找爷,以此类推直到找到祖宗。。。

全部介绍完了,下面来总结一下克鲁斯卡尔的模板吧

struct edge{
	int u,v,w,next//next在这里暂时用不上,以后学习其他图论算法会用到
	bool operator <(const edge &a)const{
		return w<a.w//这里是从小到大排序,反过来就是从大到小排序
	}
}e[100005]
int ecnt=0;//记录边数
void add(int u,int v,int w){
	e[++ecnt].u=u;
	e[ecnt].w=w;
	e[ecnt].v=v;
}
getf(int x){
return x==f[x]?x:f[x]=getf(f[x]);
}
 int kruskal(){
	int val=0,cnt=0;
	sort(e+1,e+1+m);
	for(int i=1;i<=m;i++){
		int u=e[i].u,v=e[i].v;
		int xx=getf(u);
		int yy=getf(v);
		if(xx!=yy){
			cnt++,val+=e[i].w,f[xx]=yy;
		}
		if(cnt==n-1) return val;
	}
	return -1;
}
int main(){
	cin>>n>>m;
	for(i——m){
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
	}
	for(i--n){
 		f[i]=i;//初始化,使每个点的爹都是自己
	}
}

例题讲解

例题1

营救
题目背景
“咚咚咚……”“查水表!”原来是查水表来了,现在哪里找这么热心上门的查表员啊!小明感动得热泪盈眶,开起了门……

题目描述
妈妈下班回家,街坊邻居说小明被一群陌生人强行押上了警车!妈妈丰富的经验告诉她小明被带到了 t 区,而自己在 s 区。

该市有 m 条大道连接 n 个区,一条大道将两个区相连接,每个大道有一个拥挤度。小明的妈妈虽然很着急,但是不愿意拥挤的人潮冲乱了她优雅的步伐。所以请你帮她规划一条从 s 至 t 的路线,使得经过道路的拥挤度最大值最小。

输入格式
第一行有四个用空格隔开的 n,m,s,t,其含义见【题目描述】。

接下来 m 行,每行三个整数 u, v, w表示有一条大道连接区 u 和区 v,且拥挤度为 w。

两个区之间可能存在多条大道。

输出格式
输出一行一个整数,代表最大的拥挤度。

输入输出样例
输入 #1复制
3 3 1 3
1 2 2
2 3 1
1 3 3
输出 #1复制
2
说明/提示
数据规模与约定
对于 30% 的数据,保证 n≤10。
对于 60% 的数据,保证 n≤100。
对于 100% 的数据,保证 1≤n≤10^4 ,1≤m≤2×10 ^4,w≤10 ^4,1≤s,t≤n。且从 s 出发一定能到达 t 区。
样例输入输出 1 解释
小明的妈妈要从 1 号点去 3 号点,最优路线为 1->2->3。
来看看题解

#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
int n,m;
	struct edge{
		int u,v,w;
		bool operator <(const edge &a)const{
			return w<a.w;
		}
	}e[1010101];
int s,t;
int ecnt=0;
void add(int u,int v,int w){
	e[++ecnt].w=w;
	e[ecnt].v=v;
	e[ecnt].u=u;
}
int f[1010101];
int getf(int x){
	return x==f[x]?x:f[x]=getf(f[x]);
}
int main(){
	cin>>n>>m;
	cin>>s>>t;
	for(int i=1;i<=m;i++){
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
	}
	for(int i=1;i<=n;i++){
		f[i]=i;
	}
	sort(e+1,e+1+ecnt);
	//克鲁斯卡尔
	for(int i=1;i<=ecnt;i++){
		int u=getf(e[i].u);
		int v=getf(e[i].v);
		if(u!=v){
			f[u]=v;
		}
		if(getf(s)==getf(t)){//注意这里改一下就好,这道题规定了起点终点
			cout<<e[i].w;
			return 0;
		}
	}

}

例题2

买礼物
题目描述
又到了一年一度的明明生日了,明明想要买 B 样东西,巧的是,这 B 样东西价格都是 A 元。
但是,商店老板说最近有促销活动,也就是:
如果你买了第 I 样东西,再买第 J 样,那么就可以只花 K_{I,J}元,更巧的是,K_{I,J}竟然等于 K_{J,I}。
现在明明想知道,他最少要花多少钱。
输入格式
第一行两个整数,A,B。
接下来 B 行,每行 B 个数,第 I 行第 J 个为 K_{I,J}。
我们保证 K_{I,J}=K_{J,I}并且 K_{I,I}==0。
特别的,如果 K_{I,J}
=0,那么表示这两样东西之间不会导致优惠。
输出格式
一个整数,为最小要花的钱数。

输入输出样例
输入 #1复制
1 1
0

输出 #1复制
1
输入 #2复制
3 3
0 2 4
2 0 2
4 2 0
输出 #2复制
7
说明/提示
样例解释 2。
先买第 2 样东西,花费 3 元,接下来因为优惠,买 1,3 样都只要 2 元,共 7 元。
(同时满足多个“优惠”的时候,聪明的明明当然不会选择用 4 元买剩下那件,而选择用 2 元。)
数据规模
对于 30%30% 的数据,1≤B≤ 10。
对于 100%100% 的数据,1≤B≤500,0≤A,K I,J≤1000。
提示一下,可以横距图论的想法来解决这道题,起点i,终点j,权值k[i,j]
来看题解

#include<cmath>
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>


using namespace std;
int A,B;
int mapp[1005][1005];
int f[1010101];
struct edge{
	int u,v,w,next;
	bool operator <(const edge &a)const{
		return w<a.w;
	}
}e[1010011];
int ecnt=0;
void add(int u,int v,int w){
	e[++ecnt].u=u;
	e[ecnt].v=v;
	e[ecnt].w=w;
}
int getf(int x){
	return x==f[x]?x:f[x]=getf(f[x]);
}
int main(){
	cin>>A>>B;
	for(int i=1;i<=B;i++){
		for(int j=1;j<=B;j++){
			int x;
			cin>>x;
			if(x){//需要判断一下是否优惠,不优惠直接把原价add进去
			add(i,j,x);
			}else{
				add(i,j,A);
			}
		}
	}
	for(int i=1;i<=B;i++){
		f[i]=i;
	}
	sort(e+1,e+1+ecnt);
	int cnt=0,w=0;
	//克鲁斯卡尔
	for(int i=1;i<=ecnt;i++){
		int u=getf(e[i].u);
		int v=getf(e[i].v);
		if(u!=v){
			f[u]=v;
			cnt++;
			w+=e[i].w;//熟不熟悉。。。
		}
		if(cnt==B-1){//b个点,b-1条边
			break;
		}
	}
	if(B*A<w+A){
		cout<<B*A;
	}else
	cout<<w+A;
}

这两题做完,想必各位对克鲁斯卡尔算法也有了清楚的认识,下面还有一些题目和题解可以供各位学习。今天就到这里了,再会!!!

更多克鲁斯卡尔算法题解可以看下方链接
口袋的天空,部落划分
无线通讯网
Building Roads S
拆地毯

如有疑问可以私信奥,如有错误私信我会及时做出改正。喜欢的话就关注一下吧,你的关注是我创作的动力^^

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贤鱼不闲

一分钱也是爱!!!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值