最小生成树--Kruskal(并查集应用)

嘿嘿嘿,小伙伴们,今天咱们来咬文嚼字,看看这最小生成树是个啥玩意

有趣的问答

Q1:是啥?
A1:树是连通无回路的图

Q2: 连通我知道,就是任意两个点均可达,类似于向一个点注水,整个图都有水喝;无回路嘛,应该就是不存在圈。是这样叭??
A2:真是个小机灵鬼儿哟,想不到你的理解已经如此深刻,其实呢,它还有一个隐含特性:边数比节点数少一,所以任意图想变成树,就得先控制边数,也就是毛线数只能为8-1,也就是7,他才有可能变成树哟。

Q3:那生成树是啥?在树的基础上添加限制条件??
A3: 没错,你可真聪明,生成树嘞,可以这么看,假设一个图有8个节点,把所有的节点用乒乓球表示,若是图中两个点有线相接,那就用毛线把相应的乒乓球连起来,这个乒乓球版的图就做好啦。

我们再找找生成的主语和宾语,大家习惯这么说:图 生成 树,所以图为主语,树为宾语,这其实是一个动态过程,由图转化为树,生成的含义也暗示我们树必定是图的一部分,不存在树比图多点,多边情况,然后又有大佬规定生成树的节点个数等于图的节点个数,然后树枝只能从图中已有边取。所以生成树就是在图上选出来的一棵树。若是想检验自己是否选对了生成树,就把乒乓球版图中选中的毛线留下,其余毛线全减咯,然后抓住一颗球拉倒空中,没有球落地且每一根毛线都被扯直了,那么,恭喜你,选对了

Q4:哦哦,原来如此呀,那最小生成树是不是所有生成树中边的和最短的?
A4:完全正确

Q5:若我要寻找一个有n个顶点的图的最小生成树,其实树的顶点已经确定,就是图的n个顶点;然后再选择n-1条边,只要这n-1条边和n个点是连通无回路的,那么意味着我找到了一颗生成树。只要我找出所有生成树,通过比较他们的边权之和,选出最小的那个就是最小生成树啦?
A5:小伙子,你可真是个学离散的料,这个思路是可行的,不过若是边比较多,那你的工作量就很大了

Q6:那怎么办?没有简便方法吗?
A6:嘿嘿,有滴,感谢前辈们的智慧,创造了许多方法,我给你讲讲最近刚学会的避圈法,它的英文名是Kruskal,不过还是中文比较贴切,避圈吗,就是逃避圈呗,接下来具体介绍它,come on baby~

核心–避圈

之所以给他起了个中文名–避圈法,就是它的算法特点–躲着圈。
它的思想很简单,先把图中所有点拎出来放在桌面上,边一条不拎(光秃秃的真可怜)。其次将边按权值从小到大排个序,放到一个只出不进的箱子(真大方)里。接着每次从只出不进的箱子取出一条最小边,若是该边的两个顶点在桌面上已连通,就丢了这条边(不许放回箱子);若是两个顶点在桌面上八竿子打不着,就用这条边将桌面上两点连起来。只要向桌面加入(节点数 - 1)条边,恭喜你,大功告成咯

从它的思想描述中可以抽出两个关键步骤:

  • 1,对边按权值从小到大排序(随便来个排序算法,详细可参考八种排序
  • 2,判断两点是否连通(利用并查集,详细可参考并查集

实现思路

数据结构

typedef struct{
	int n,m;//定点数,边数 
}Graph;//图

typedef struct{
	int u,v,w;//起止点,权值 
	bool MST;//判断是否被选为最小生成树的树枝 
}Edge;//边结构 
Graph G;//全局定义,图 
Edge* edge;//边集,动态数组 

先啃并查集

并查集核心在于并(合并),查(查找),初始化也很重要
详细可参考上文链接,此文重心在于求最小生成树

查找

//查找child所在子树的根节点,并压缩路径,即将该子树中所有的点直接指向根 
int Find(int child,int* parent)
{
	//查找所在子树的根节点 
	int f = child;
	while(parent[f] > 0){
		f = parent[f];
	}
	//压缩路径 
	int j = child;
	while(j != f){
		parent[j] = f;
		j = parent[j];
	}
	return f;
}

合并

//合并两个子树的根 
void Union(int a,int b,int* parent)
{
	//权值小的做根,优化 
	if(edge[a].w > edge[b].w){
		parent[a] += parent[b];
		parent[b] = a;
	}
	else{
		parent[b] += parent[a];
		parent[a] = b;
	}
}

边排序

对关键字:边权,进行简单选择排序

//根据边权升序排列:简单选择排序 
void SortEdge()
{
	for(int i = 0; i < G.m; i++){
		int k = i;//cout<<i<<endl;
		for(int j = i+1; j < G.m; j++){
			if(edge[k].w > edge[j].w){
				k = j;
			}
		}
		if(k != i){
			Swap(edge[k],edge[i]); 
		}
	}
	TraverseEdge();//打印调试
}

集成

前边难点已经被攻克,零件以准备好,这里只需要组装即可
结合注释很容易的(偷个懒~)

//克鲁斯卡尔算法 
void Kruskal()
{
	InitEdge();//初始化边 
	SortEdge();//边升序排列 
	int* parent;//并查集准备:记录当前点对应的双亲节点 
	parent = (int*)malloc(G.n*sizeof(int));
	for(int i = 1; i <= G.n; i++){//从下标1开始,初始化为-1 
		parent[i] = -1;
	}
	int j = 0;//控制边的下标:第j条边 
	for(int i = 0; i < G.n-1; i++){//选取n-1条边 
		while(true){TraverseEdge();
			int u,v;
			u = edge[j].u;
			v = edge[j].v;
			int fa,fb;
			fa = Find(u,parent);//寻找u,v的根节点 
			fb = Find(v,parent);
			if(fa == fb)j++;//u,v在同一连通分支,选择下一条边 
			else{//不连通 
				Union(fa,fb,parent); 
				edge[j].MST = true;//表示被选中 
				j++;//下一条边 
				break;//跳出,进入下一条边的选择 
			}
		}
	}
	cout<<endl<<"======最小生成树======"<<endl;
	for(int i = 0; i < G.m; i++){
		if(edge[i].MST){
			cout<<"u:"<<edge[i].u<<" v:"<<edge[i].v<<" w:"<<edge[i].w<<endl;
		}
	}
} 

小收获

  • 不惧挑战:以前一直以为克鲁斯卡尔算法很难,因为老师在课上说这个不好写,直接掌握Prim就好,这就把我给劝退了,直到昨天写数构课设时有一道求解朋友圈的题目,一开始真不会,上文求索后,原来关键在于一个并查集呀,它也是种数据结构,只闻其名,不见其人,一直以为他是很高深的算法呢,原来简单的不得了,学会它后自然要触类旁通,顺便解决下Kruskal这个心魔,嘿嘿嘿写出来还是比较顺利的呀
  • 最近事情比较多,感觉有些浮躁,学习时无法静心凝神,深入问题本质,希望尽快调整心态,勿忘初衷,什么都别想太多,小伙子,人生路很长,你好好走就是~共勉之
  • 以后每天背一首诗,天天在计算机的世界里都快失去感性咯
  • 生活要慢,做事要快,两者分得开些,焦虑少一些

完整Code

从文件读入的数据
文件最小生成树.txt内容(第一行为顶点,边个数;其余行每三个数表示点顶点u,顶点v,权值w)

6 10
0 1 6 0 2 1 0 3 5
1 2 5 1 4 3
2 3 5 2 4 6 2 5 4
3 5 2
4 5 6

结果
在这里插入图片描述

#include<iostream>
using namespace std;
#include<stdlib.h>
#include<fstream>

typedef struct{
	int n,m;//定点数,边数 
}Graph;

typedef struct{
	int u,v,w;//起止点,权值 
	bool MST;//判断是否被选为最小生成树的树枝 
}Edge;//边结构 
Graph G;//全局定义,图 
Edge* edge;//边集,动态数组 
//交换两条边 
void Swap(Edge &e1,Edge &e2)
{
	int t;
	t = e1.u;e1.u = e2.u;e2.u = t;
	t = e1.v;e1.v = e2.v;e2.v = t;
	t = e1.w;e1.w = e2.w;e2.w = t;
}
//打印调试用的 
void TraverseEdge()
{
	for(int i = 0; i < G.m; i++){
		cout<<edge[i].u<<" "<<edge[i].v<<" "<<edge[i].w<<endl;
	}
}
//根据边权升序排列:简单选择排序 
void SortEdge()
{
	for(int i = 0; i < G.m; i++){
		int k = i;cout<<i<<endl;
		for(int j = i+1; j < G.m; j++){
			if(edge[k].w > edge[j].w){
				k = j;
			}
		}
		if(k != i){
			Swap(edge[k],edge[i]); 
		}
	}
	TraverseEdge();
}
void InitEdge()
{
	fstream inFile("最小生成树.txt",ios::in);
	if(!inFile)cout<<"fail to open file!"<<endl;
	inFile>>G.n>>G.m;
	edge = (Edge*)malloc(G.m*sizeof(Edge));
	for(int i = 0; i < G.m; i++){
		inFile>>edge[i].u>>edge[i].v>>edge[i].w;
		edge[i].u += 1;//输入的点从0开始,存储时从1开始 
		edge[i].v += 1;
		edge[i].MST = false; 
	}
//	TraverseEdge();
	inFile.close();
}
//查找child所在子树的根节点,并压缩路径,即将该子树中所有的点直接指向根 
int Find(int child,int* parent)
{
	//查找所在子树的根节点 
	int f = child;
	while(parent[f] > 0){
		f = parent[f];
	}
	//压缩路径 
	int j = child;
	while(j != f){
		parent[j] = f;
		j = parent[j];
	}
	return f;
	
}
//合并两个子树的根 
void Union(int a,int b,int* parent)
{
	//权值小的做根,优化 
	if(edge[a].w > edge[b].w){
		parent[a] += parent[b];
		parent[b] = a;
	}
	else{
		parent[b] += parent[a];
		parent[a] = b;
	}
}
//克鲁斯卡尔算法 
void Kruskal()
{
	InitEdge();//初始化边 
	SortEdge();//边升序排列 
	int* parent;//并查集准备:记录当前点对应的双亲节点 
	parent = (int*)malloc(G.n*sizeof(int));
	for(int i = 1; i <= G.n; i++){//从下标1开始,初始化为-1 
		parent[i] = -1;
	}
	int j = 0;//控制边的下标:第j条边 
	for(int i = 0; i < G.n-1; i++){//选取n-1条边 
		while(true){TraverseEdge();
			int u,v;
			u = edge[j].u;
			v = edge[j].v;
			int fa,fb;
			fa = Find(u,parent);//寻找u,v的根节点 
			fb = Find(v,parent);
			if(fa == fb)j++;//u,v在同一连通分支,选择下一条边 
			else{//不连通 
				Union(fa,fb,parent); 
				edge[j].MST = true;//表示被选中 
				j++;//下一条边 
				break;//跳出,进入下一条边的选择 
			}
		}
	}
	cout<<endl<<"======最小生成树======"<<endl;
	for(int i = 0; i < G.m; i++){
		if(edge[i].MST){
			cout<<"u:"<<edge[i].u<<" v:"<<edge[i].v<<" w:"<<edge[i].w<<endl;
		}
	}
} 
int main()
{
	Kruskal();
	return 0;
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值