最小生成树(MST)

目录

一、知识点

二、例题


一、知识点

1. 生成树定义:在一个有n个点的无向连通图中,取其n-1条边并连接所有的顶点,所得到的子图称为原图的一棵生成树。

2. 树的属性:无环+连通+任意两点之间只有唯一的简单路径+删掉任意边就不连通

3. 最小生成树:各边权和最小的一棵生成树。

4. 最小边原则:图中权值最小的边(如果唯一的话)一定在最小生成树上

5. 唯一性定理:对于一个图,如果各边权值不等,则图的MST一定是唯一的,反之不成立


计算无向图的最小生成树

1. Prime

算法思路:贪心
(1)最初将无向连通图分成两个顶点集合A、B,任选一个顶点a先放到A,将B中与a有关并且权值最小的点加到A,知道n个顶点全部属于A结束。

  • 注意这里的d数组,存的是到树的最短路径,不是到源点的最短路径。这里注意区别单源最短路径。所以,每次更新d数组时,比较的是d[i]与g[k][i]而不用加上ans。

(2)显然出发点不同,最小生成树的形态就不同,但边权和的最小值是唯一的。

复杂度:O(N^2)

//prime算法 
#include<bits/stdc++.h>
using namespace std;

const int inf=0x3f3f3f3f;
const int maxn=505;
int vis[maxn];//标记顶点i是否加入最小生成树中
int d[maxn];//表示点i与当前生成树中的点有连边的边长的最小值
int g[maxn][maxn];//存边权
int n,m,ans;

void read()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)g[i][j]=inf;
	for(int i=1;i<=m;i++)
	{
		int x,y,w;
		scanf("%d%d%d",&x,&y,&w);
		g[x][y]=g[y][x]=w;
	}
}
void Prim(int v0)
{
	memset(vis,0,sizeof(vis));//初始化生成树点集
	for(int i=1;i<=n;i++)d[i]=inf;
	d[v0]=0;ans=0;
	int minn,k;
	for(int i=1;i<=n;i++)//选择n个点
	{
		minn=inf;
		for(int j=1;j<=n;j++)
			if(!vis[j] && minn>d[j])
			{ 
				minn=d[j];
				k=j; 
			}
		vis[k]=1;//标记;
                ans+=d[k];//算最小生成树的边权和
		for(int j=1;j<=n;j++)//修改d数组
			if(!vis[j] && d[j]>g[k][j])//这里注意区别单源最短路径
				d[j]=g[k][j]; 
	 } 
} 
int main()
{
	read();
	Prim(1);
	cout<<ans<<endl;
	return 0;
}

2. Kruskal

算法思路:贪心
(1)将图中的所有边都去掉。
(2)将边按权值由小到大添加到图中,并保证添加的过程中不会形成环
(3)重复上一步直到连接所有顶点

该方法用到了并查集来判断是否会产生环

复杂度:O(mlogm+mα(n) )   //α(n)是一次并查集的复杂度

//kruskal算法
#include<bits/stdc++.h>
using namespace std;

const int maxn=1e5+10;

struct edge{
	int x,y,z;
}a[maxn];

bool cmp(edge x,edge y)
{
	return x.z<y.z;
}

int n,m,pre[maxn],ans,flag;

int findd(int x)
{
	if(pre[x]==x)return x;
	pre[x]=findd(pre[x]);
	return pre[x];
}
void kruskal()
{
	for(int i=1;i<=n;i++)pre[i]=i;
	int k=0;
	for(int i=1;i<=m;i++)
	{
		int f1=findd(a[i].x);
		int f2=findd(a[i].y);
		if(f1!=f2)
		{
			ans+=a[i].z;
			pre[f1]=f2;
			k++;
			if(k==n-1)break;//最小生成树的边数为n-1 
		}
	}
	if(k<n-1)
	{
		puts("impossiable");
		flag=0;
		return;
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	ans=0;flag=1;
	for(int i=1;i<=m;i++)
		scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
	sort(a+1,a+1+m,cmp);
	kruskal();
	if(flag)printf("%d\n",ans);
	return 0;
}

二、例题

1. 【loj】#10064. 「一本通 3.1 例 1」黑暗城堡(最短路径生成树 dijkstra+Prim)

2. 【loj】#10066. 「一本通 3.1 练习 1」新的开始 (最小生成树·Prim)

3. 【loj】#10067. 「一本通 3.1 练习 2」构造完全图(最小生成树 Kruskal)

题目描述:

对于完全图 G,若有且仅有一棵最小生成树为 T,则称完全图 G 是树 T 扩展出的。

给你一棵树 T,找出 T 能扩展出的边权和最小的完全图 G。

题目链接:https://loj.ac/problem/10067

【分析】给出最小生成树,并且说明了该MST形态唯一。类比Kruskal算法。并查集。

先把边按权值由小到大排序,然后遍历边。注意,边数是n-1;

把图的顶点集分为两个集合。集合中的点数size[x], size[y],因为是完全图,所以任意两点之间都是有边直接相连的。

所以连通两个集合使其变成完全图一共需要cnt=size[x]*size[y]条边

而遍历边的时候,已经存在一条,所以只需要再加cnt-1条边即可。而边权比新加入的这条边的边权+1。

所以核心式子就是(size[x]*size[y]-1)*(a[i].d+1)

注意计算的时候,要强制转换为long long....不然!就一直wa....╥﹏╥

这里讲得很棒!

【代码】

#include<bits/stdc++.h>
using namespace std;

const int maxn=1e5+10;
typedef long long ll;

int pre[maxn],size[maxn];
ll ans,n;

struct node{
	int l,r,d;
}a[maxn];
bool cmp(node x,node y)
{
	return x.d<y.d;
}

int findd(int x)
{
	return x==pre[x]?x:pre[x]=findd(pre[x]);
}

int main()
{
	ans=0;
	scanf("%lld",&n);
	for(int i=1;i<n;i++)
	{
		scanf("%d%d%d",&a[i].l,&a[i].r,&a[i].d);
		pre[i]=i;size[i]=1;
		ans+=a[i].d;	
	}
	pre[n]=n;size[n]=1;
	sort(a+1,a+n,cmp);//n-1条边
	for(int i=1;i<n;i++)
	{
		int x=findd(a[i].l);
		int y=findd(a[i].r);
		if(x!=y)
		{
			ans+=(ll)(size[x]*size[y]-1)*(a[i].d+1);//这里!!!wa了好几次的起源...
			pre[x]=y;
			size[y]+=size[x];
		}
	 } 
	printf("%lld\n",ans);
	return 0;
}
 
 
 

猜你喜欢

转载自blog.csdn.net/qq_38735931/article/details/84590963