最小生成树两种算法详解

最小生成树

众所周知, 树是一种特殊的图, 是由n-1条边连通n个节点的图.

如果在一个有n个节点的无向图中, 选择n-1条边, 将n个点连成一棵树, 那么这棵树就是这个图的一个生成树.

如果保证树的边权和最小, 那么这棵树就是图的最小生成树.

为了求一棵树的最小生成树, 有两种算法, 一种是选择点加入树的Prim算法, 另一种是选择边加入树的Kruskal算法.

Prim算法

这个算法的过程和Dijkstra类似, 但有所不同.

首先选择任意一点作为树的第一个节点0, 枚举与它相连的所有点i, 将两点之间的边权记为这个点到生成树的距离b[i], 选择距离最近的点加入生成树, 然后枚举与之相邻的节点j, 用边权a[i,j]更新b[j], 使其等于min(b[j],a[i,j]), 这样再继续加入当前离生成树最近的点, 在更新它相邻的点, 以此类推, 直到所有点全部加入生成树. 这样, 便求出了最小生成树.

关于正确性

我自己的思路是这样的: 如果用Prim算法求出了一棵最小生成树, 将一条边u换成另一条更小的v, 就得到一棵边权和更小的生成树. 首先保证树连通, 所以去掉u和v, 生成树被分成两个连通块是一模一样的. 在当时连接u的时候, 已经决策完的生成树一定也和v相连, 这时v连接的节点一定会比u连接的节点更早加入, 所以一开始的假设不成立, 算法正确.

具体代码实现

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
int n,m,l,r,x,a[5005][5005]/*邻接矩阵*/,b[5005]/*点到生成树的最短边权*/,now/*当前加入的点*/,k=1/*生成树节点数*/,ans=0/*生成树总边权和*/;
bool vsd[5005]={0};
void update(int at){//用节点at更新其他点的b[]值
	for(int i=1;i<=n;i++) {
		b[i]=min(a[at][i],b[i]);
	}
	vsd[at]=true;
	return;
}
int find(){//寻找当前离生成树最近的点
	int ft=0;
	for(int i=1;i<=n;i++){
		if(!vsd[i]){//不在树中
			if(b[i]<=b[ft]){
				ft=i;
			}
		}
	}
	return ft;
}
int main(){
	cin>>n>>m;
	memset(a,0x3f,sizeof(a));
	for(int i=1;i<=n;i++){
		a[i][i]=0;
	}
	for(int i=1;i<=m;i++){
		cin>>l>>r>>x;
		a[l][r]=min(a[l][r],x);//防止有两个点之间出现边权不同的几条边
		a[r][l]=min(a[r][l],x);
	}
	memset(b,0x3f,sizeof(b));
	update(1);
	while(k<n){//加入n-1个点后返回(第一个点本来就在树中, 无需加入)
		now=find();//加入最近的点now
		ans+=b[now];//统计答案
		update(now);//更新其他点
		k++;//统计点数
	}
	cout<<ans<<endl;
	return 0;
}

Kruskal算法

这个算法和Prim相反, 它是将边记为树上的边, 最终得到一棵最小生成树.

将所有边按边权排序, 然后将它们从小到大讨论是否加入生成树. 如果该边的两个端点属于同一个连通块, 这时加入该边就会形成环, 不符合树的定义, 所以舍弃. 如果该边两个端点不属于同一个连通块, 那么连接该边, 将两个端点所在连通块连成一个.

当共加入n-1条边的时候, 就得到了一棵最小生成树.

对于查找两点是否在同一个连通块中的方法, 我们可以使用并查集来维护点之间的连通关系.

扫描二维码关注公众号,回复: 11213088 查看本文章

正确性简易说明

Kruskal相对来说更好理解, 因为从小到大排序后, 使用被舍弃的边连成环是非法的, 使用排在后面的合法的边替换已经选择的边, 得到的答案不是最优的. 所以Kruskal算法正确.

代码实现

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n,m,fa[10005],s,e,l,k=0,ans=0;
struct side{
	int le,ri,len;//起点, 终点, 边权
}a[200005];
bool cmp(side x,side y){//结构体sort规则
	return(x.len<y.len);
}
int find(int x){//并查集寻找最老祖先
	if(fa[x]==x){//自己就是当前连通块最老祖先
		return x;
	}
	fa[x]=find(fa[x]);//自己祖先的最老祖先
	return fa[x];
}
int main(){
	cin>>n>>m;
	memset(a,0x3f,sizeof(a));
	for(int i=1;i<=m;i++){
		cin>>s>>e>>l;
		a[i].le=s;//结构体存储边
		a[i].ri=e;
		a[i].len=l;
	}
	sort(a+1,a+m+1,cmp);//按边权升序排列
	for(int i=1;i<=n;i++){
		fa[i]=i;//初始化并查集
	}
	int i=0;
	while((k<n-1/*加入了n-1个点跳出*/)&&(i<=m/*枚举完了所有的边跳出*/)){
		i++;
		int fa1=find(a[i].le),fa2=find(a[i].ri);//两个端点的最老祖先
		if(fa1!=fa2){//不在同一连通块
			ans+=a[i].len;//记录答案
			fa[fa1]=fa2;//连接连通块
			k++;//记录边数
		}
	}
	cout<<ans<<endl;
	return 0;
}

之前发的是笔记, 现在发的是实战总结

猜你喜欢

转载自www.cnblogs.com/Wild-Donkey/p/12905633.html
今日推荐