克鲁斯卡尔算法(Kruskal Algorithm)——图的最小生成树

听骚话讲作者

[手动滑稽]
其实早就该写这篇博客啦。

图的最小生成树

一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。最小生成树可以用kruskal(克鲁斯卡尔)算法或Prim(普里姆)算法求出。
换句话说,就是使原图变成一棵树并且要求这棵树中保留的边最小。(变树的过程需要删除边)
如下图,红线为此图的一个最小生成树:
在这里插入图片描述

浅谈克鲁斯卡尔算法

克鲁斯卡尔算法是一种用来寻找最小生成树的算法。在剩下的所有未选取的边中,找最小边,如果和已选取的边构成回路,则放弃,选取次小边。
根据克鲁斯卡尔算法定义,我们要解决以下问题:

1.选择多少条这样的边?
2.使用什么思想来解决?(贪心,动态规划,数论,分治,递归,递推。。。。。。)

对于第一个问题,我们很容易可以得到答案。假设有n个点,那么必然保留n-1条边,只有这样才满足树的性质。

满足了树的性质,我们还要满足“最小”的性质。
在克鲁斯卡尔算法中,我们选择使用贪心法来解决。

我们为了取用最小的边,所以对所有的边进行排序,并从最小的开始选取。
为此,我们需要一个存储边以及边权的表,如下图:

struct e{
	int u,v,order,s;
	e(){u=v=order=0;return;}
	bool operator < (const e &x)const{
		if(s<x.s) return 1;
		return 0;
	}
}edge[maxm];

这里使用了运算符重载,因为我们要的只是对边权排序,而其他所有和边有关的变量一起被排序。
对这种捆绑排序和运算符重载不理解的可以戳这里:戳我手动滑稽一下
使用下面这个函数有助于你往边表里添加边,当然你可以不写这个函数。

void addedge(int u,int v,int s,int order){  //这不是邻接表! 
	edge[++js].u=u;
	edge[js].v=v;
	edge[js].s=s;
	edge[js].order=order;
	return;
} 

这看起来很像邻接表,但是这绝对不是邻接表。这里没有链表的象征——后继指针next
如果你对我上述的话有质疑,那你一定要看这篇——邻接表存图法
这绝对不会浪费你人生的十分钟

接下来我们优美的排个序:

sort(edge+1,edge+s+1);

在这里插入图片描述
然而,当我们从最小的开始选取时,选完最小的两边——(1,2)和(1,3)时,有可能就会选到上图中标蓝色的边。而一旦选取了这个边,就会构成回路,这样就不满足树的性质了。
为了防止其形成回路,我们需要使用一个数据结构——并查集

不懂并查集的朋友点我

每当我们加入一条边时,便把这个边的两个端点加入并查集。然后后续选择边的时候,需要把即将添加的边的两个端点进行查找,如果不在同一个并查集里头,就可以加入该边,否则就跳过这条边而选择下一条次小的。
流程这样描述:

for(int i=1;i<=s;i++){
		if(time==p-1) break;
		if(get(edge[i].u)!=get(edge[i].v)){
			merge(edge[i].u,edge[i].v);
			cout<<edge[i].u<<" link with "<<edge[i].v<<endl;
			time++;
		}
	} 

其中if(time==p-1) break; 就是当已经选择了p-1条边时(假设p为总点数)自动退出循环。
if(get(edge[i].u)!=get(edge[i].v)) 这个就是判断是否形成环路的,通过这层判断就代表不在同一个并查集里。
我们这里用cout<<edge[i].u<<" link with "<<edge[i].v<<endl; 来输出我们被选中的边。time是用于统计被选中的边数。
这样,克鲁斯卡尔基本上就搞定了

代码示例

样例输入样例输出
6 101 link with 2
1 2 11 link with 3
1 3 12 link with 5
2 3 24 link with 5
2 4 34 link with 6
2 5 2
3 5 2
3 4 3
4 5 2
4 6 3
5 6 3
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxm=10001;
int p,s; 
int a,b,c;
struct e{
	int u,v,order,s;
	e(){u=v=order=0;return;}
	bool operator < (const e &x)const{
		if(s<x.s) return 1;
		return 0;
	}
}edge[maxm];

int js;
void addedge(int u,int v,int s,int order){  //这不是邻接表! 
	edge[++js].u=u;
	edge[js].v=v;
	edge[js].s=s;
	edge[js].order=order;
	return;
} 

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

void merge(int x,int y){
	x=get(x);y=get(y);
	if(x!=y) fa[y]=x;
	return;
} 

int time;
void kruskal(){
	sort(edge+1,edge+s+1);
	for(int i=1;i<=s;i++){
		if(time==p-1) break;
		if(get(edge[i].u)!=get(edge[i].v)){
			merge(edge[i].u,edge[i].v);
			cout<<edge[i].u<<" link with "<<edge[i].v<<endl;
			time++;
		}
	} 
	return;
}

int main(){
	std::ios::sync_with_stdio(false);
	cin>>p>>s;
	for(int i=1;i<=s;i++){
		cin>>a>>b>>c;
		addedge(a,b,c,i);
	}for(int i=1;i<=p;i++)
		fa[i]=i;
	
	kruskal();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值