专题·最小生成树【including Kruskal、Prim

初见安~

最小生成树

顾名思义,最小生成树就是求一个边带权的连通图里边的权值之和最小的一个树。

所以一棵树就自然而然的,它本身就是它的最小生成树了。

【图丑勿喷】

在上图中【有假的三角形,假装没看到吧。】,最小生成树为:

那么怎么求呢~


Kruskal算法

一般求最小,我们首先的思路都是贪心——从最小的边开始找。当然这个思路是可行的——Kruskal就是这么做的。

既然要找最小的边,那么我们可以读入所有的边然后排个序再来看;但是很快就出现了一个问题:树的边数为n-1,怎么确保选入的最小的边没有重复连,没有漏连?这里就需要判断一下:这条边要连接的两个点是否已经有其他的边连起来了?直接or间接都算。所以我们最好开一个结构体方便存一条边。当然这里就会涉及到另一个算法思想——并查集【这里是传送门:专题·并查集】。说到并查集就很简单了:只要判断这两个点是不是在同一个集里面就可以决定连不连这条边了。嗯就是这么简单。【惊不惊喜!意不意外!】

接下来是模板代码:

#include<bits/stdc++.h>
using namespace std;
int fa[5010],n,m,ans=0;
struct edge
{
	int u,v,w;
}e[200005];

bool cmp(edge a,edge b)//不会写operator就写了和bool……
{
	return a.w<b.w;
}

int get(int x)
{
	if(fa[x]==x) return x;//当x的根为它自己时,这就是这一个集的根了。
	return fa[x]=get(fa[x]);//顺便路径压缩一下
}

int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		fa[i]=i;//一开始各自独立,要初始化
	for(int i=1;i<=m;i++)
	{
		cin>>e[i].u>>e[i].v>>e[i].w;
	}
	
	sort(e+1,e+1+m,cmp);
	//其实可以再开一个cnt来计数,就不用循环到m次,n-1次就可以了。
	for(int i=1;i<=m;i++)
	{
		int u=get(e[i].u);//找到根(祖先)
		int v=get(e[i].v);
		if(u==v) continue;
		fa[u]=v;//v连u和u连v其实都无所谓,反正祖先都是同一个。
		cout<<e[i].u<<" "<<e[i].v<<endl;//这是我画图的时候用的语句……
		ans+=e[i].w;
	}
	cout<<ans<<endl;
	return 0;
}
/*
上图的数据:
7 9
1 6 10
1 3 2
1 2 5
6 7 7
5 6 8
3 5 4
2 5 15
2 4 10
4 5 6
输出:
32
*/

这就是一个很好理解的算法了:)


Prim算法

这一算法的话同样是基于Kruskal的推论(放心吧跟并查集关系不大),但思想略有不同:我们可以将选在树里面的点和没有选的点各自划分为两个集合T和S,即一开始T为空,S有n个点。 毕竟求的是树,所以从哪个点开始一般是无所谓的,就从1开始吧——T={ 1 },S={ x | 2<=x<=n };

接下来开始连边——很明显:我们的意图是从T的点连出去的边中找一条较短的连到S中的某个点,并把那个点拖到T中来。不同于Kruskal的就是,Prim天然保证在找边的时候是连着找的,即T中的点都是连通的。每T里添加一个点,ans就加这条边的权值。

看到这里你还是要冷静——用set肯定是没有必要的,这里就真的要用个bool vis[ ] 了:标记是否选中即可。接下来我们还是要运用一个贪心的思想——圈外的的点中,将和选中的这个圈最近的一个点拉进圈里,再更新其他没有选的点和这个圈的距离,再找最小的。以此类推。所以为了方便找某两条边之间的距离,我们用邻接矩阵来存。不要以为邻接矩阵总很容易爆,其实在边多的时候Kruskal反而容易爆。因而可以得出结论:一般稠密图甚至完全图,建议用Prim算法。

Prim的话(毕竟我也尽力了)时间复杂度为O(n²),可以基本保证不会做多余的循环(当然Kruskal还是要简单得多)。

以下是模板代码——

#include<bits/stdc++.h>
using namespace std;
int m,n,d[5010],a[5010][5010],ans=0;
bool vis[5010];
void prim()
{
	memset(d,0x3f,sizeof d);
	d[1]=0;//d存的是点到已经选了的圈最短的距离
	for(int i=1;i<n;i++)//循环n-1次,选n-1条边
	{
		int x=0;
		for(int j=1;j<=n;j++)
			if(!vis[j]&&(x==0||d[j]<d[x])) x=j;//判断是否为圈外点及是否是离圈最近的点
			
		vis[x]=1;//i=1的循环中,d数组除了1都是极大值,所以自然而然地就先选中了1
		for(int j=1;j<=n;j++)
		{
			if(!vis[j]) d[j]=min(d[j],a[x][j]);//每圈一个点,就更新一遍:各个圈外点到圈内的最短距离
		}
	}
}

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++)
	{
		int x,y,z;
		cin>>x>>y>>z;
		a[y][x]=a[x][y]=min(a[x][y],z);//取min是为了避免两点之间有两条不同长的路 
	}
	
	prim();
	
	for(int i=2;i<=n;i++)
	{
		ans+=d[i];
	}
	cout<<ans<<endl;
}

以上就是最小生成树的两种算法啦——

迎评:)

——End——

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值