最小生成树那些事

398 阅读8分钟

目录

最小生成树是啥?

树是啥?

最小是啥最小?

应用场景:

两大算法介绍:prim普里姆算法&Kruskal克鲁斯卡尔算法

该怎么想?

克鲁斯卡尔算法:

普里姆算法:

例题与思路:

Networking

Highways

Arctic Network

写在最后:模版

prim模版:

Kruskal模版:

参考文章:


最小生成树是啥?

树是啥?

顾名思义:树,当然要长得像树,当然要满足几个成为树的条件(这些条件看一看有个印象就行了):

1. 每个节点有零个或多个子节点;

就好像树的顶端,有的是枝桠,可以继续有叶子,有的是叶子,不能再有叶子了。

2. 没有父节点的节点称为根节点;

来个图你就懂了(图中,A就是根节点,因为它上面没人了~)

  1. 对于不是根节点的节点来说,它只能有一个父节点,比如B只有A,H只有C。

  2. 除了根节点,其他的节点都可以分成小树,就像根上的枝桠,枝桠上的叶子,但是要注意,叶子或者说枝桠之间,不能够连起来,你长你的,我长我的,要是连起来就不是树了,就是图了,就像近亲结婚一样要不得(笑;

「所以树里面不能连起来,就是说树中不能有环」

最小是啥最小?

先看看百度的定义:“一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。”

是不是懵了?

想理解这个不妨先下去看“应用场景。”

懂了用在哪里,再学就知道为啥了。

用应用场景的例子,n个城市铺设光缆,肯定是越省钱越好,也就是,我要把这些城市连起来,还要花的钱最少,这样说这个例子理解了吧?

花的钱少,对应到树里面,就是一个点到另一个点的距离短,那最小生成树,就是在所有点的各种连法中,选一种花钱最少,也就是距离总和最短的一棵树。

放到树里面,这个距离就叫权值,那不管三七二十一,我只要一个权值距离最短的就好了。

而权值和最短的树,就是最小生成树了。

应用场景:

要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。

两大算法介绍:prim普里姆算法&Kruskal克鲁斯卡尔算法

该怎么想?

抛开算法不谈,你拿到这个问题(光缆)想怎么解决?

那当然就是把我得到的所有城市之间的距离拿出来

排个序,然后从小的线路开始建,对吧?

巧了,克鲁斯卡尔算法也是这么个思路。

那我们先来看看吧。

克鲁斯卡尔算法:

将一颗颗小树合并成一棵大树,即从权值最小的边开始收录,将两个节点合并到一起,慢慢合并直到所有的小树合并在一起。

步骤:

1.边长排序

2.从小到大添加边长

3.判断(详细看下面)

画个图举个例子:

这个例子里,题目给了我两个点1,2和三条边,分别长为37km,17km,68km 

怎么做?

首先按三条边的长度,从小到大进行排序。

然后让2-1(17)这条边相连,

注意!

相连之后要做两个超关键的事情:

(1)判断边数是不是n-1(n是点数也就是2)

(2)判断有没有环(之前说了有环了就是图,不能是数,所以这个一定要注意)

这样做完之后就可以轻松得到答案了。

「注意这些边是没有方向的,所以存的时候两边都要存」

 不是很理解?

那再来个例子!

老样子,n是点,m是边。

排序

对每一步进行的操作如下,细细看图即可。 

普里姆算法:

让一棵小树慢慢长大,即从一个根节点开始,一个个的添加节点到最小生成树上。添加的过程中,要满足以下几个条件:

1.添加的节点一定是和当前的生成树有相连边的节点

2.一定是离当前树距离最近的节点(贪心)

3.不能有回路

prim算法就是把连入树的一段节点看成一个整体,而没连入这个整体的其他点,与这个整体相连的部分,就是需要讨论的「根据以上条件思考即可」。

例题与思路:

Networking

题意:(去poj搜)和上面说的应用差不多,只是多了“多组样例。”

AC代码:

#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>

#define int long long
using namespace std;
const int INF = 0x3f3f3f3f3f;
const int N = 505;
int a[N][N],dist[N];
bool vis[N];//用来判断走没走过
int n,m,sum;
int u,v,w;

int prime(int pos)
{
	dist[pos]=0;
	for(int i=1;i<=n;i++)
	{
		int cur = -1;
		for(int j=1;j<=n;j++)
			if(!vis[j]&&(cur==-1||dist[j]<dist[cur])) cur = j;
	if(dist[cur]>=INF) return INF;
	sum += dist[cur];
	vis[cur]=true;
	for(int k=1;k<=n;k++)
		if(!vis[k]) dist[k] = min(dist[k],a[cur][k]);
	}
	return sum;
}

signed main()
{
	while(scanf("%lld",&n)!=EOF){
		if(n==0) break;
		cin>>m;sum=0;
		memset(a, 0x3f, sizeof(a));
		memset(dist, 0x3f, sizeof(dist));
		memset(vis, false, sizeof(vis));
		for(int i=1;i<=m;i++)
			{
				cin>>u>>v>>w;
				a[u][v]=min(a[u][v],w);
				a[v][u]=min(a[v][u],w);
			}
		int val = prime(1);
		if(val>=INF) puts("impossible");
		else cout<<sum<<endl;
	}
	return 0;
}

Highways

 题意:

同样给我n个点和m条边

只不过这一次不是让你求最小生成树的权值和了,是要求路径!

有几个点需要注意「见代码」

(1)如何读入长度

(2)prim板子预处理

(3)记录之前走的位置

AC代码:

#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
#define int long long
using namespace std;
const int INF = 0x3f3f3f3f3f;
const int N = 1e3+10;
int a[N][N],dist[N],x[N],y[N],pre[N];
bool vis[N];
int n,m,sum=0;
int u,v,w;
//dist[i]数组存还未处理的城市i离已经处理过的城市的最短距离,
//void prime()
//{
//	for(int i=1;i<=n;i++)
//	{
//		pre[i]=1;
//		dist[i]=a[1][i];
//	}
//	dist[1]=-1;
//	for(int i=1;i<n;i++)
//	{
//		int cur = -1;
//		for(int j=1;j<=n;j++)
//			if(dist[j]!=-1&&(cur==-1||dist[j]<dist[cur])) cur = j;
//		if(dist[cur]>=INF) return;
//		if(dist[cur]!=0) printf("%lld %lld\n",pre[cur],cur);
//		dist[cur]=-1;
//		for(int k=1;k<=n;k++)
//		{
//			if(a[k][cur]<dist[k]){
//				dist[k]=a[k][cur];
//				pre[k]=cur;
//			}
//		}
//	}
//}
void prime()
{
	for(int i=1;i<=n;i++)
	{
		pre[i]=1;
		dist[i]=a[1][i];
	}
	vis[1]=true;
	for(int i=1;i<n;i++)
		{
			int cur = -1;
			for(int j=1;j<=n;j++)
				if(!vis[j]&&(cur==-1||dist[j]<dist[cur])) cur = j;;
			if(dist[cur]!=0) printf("%lld %lld\n",pre[cur],cur);
			vis[cur]=true;
			for(int k=1;k<=n;k++)
				if(!vis[k]&&a[k][cur]<dist[k])
				{
					dist[k]=a[k][cur];
					pre[k]=cur;
				}
	}
//	return sum;
}
signed main()
{
	cin>>n;
	memset(a, 0x3f, sizeof(a));
	for(int i=1;i<=n;i++)
		cin>>x[i]>>y[i];
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<i;j++)
			a[i][j]=a[j][i]=(x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]);
		a[i][i]=INF;
	}
	cin>>m;
	for(int i=1;i<=m;i++)
	{
		cin>>u>>v;
		a[u][v]=a[v][u]=0;
	}
	prime();
	
	return 0;
}

Arctic Network

 题意:同样是一个比较核善的最短路,只不过这次求的东西又不一样了,最小生成树中所有的路径,卫星要占据大的部分(此处sort排序解决),然后求剩下的最大的长度。

AC代码:

#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
#define int long long

using namespace std;
const int INF = 0x3f3f3f3f3f;
const int N = 505;
double a[N][N],dist[N];
bool vis[N];//用来判断走没走过
double sum=0;
int u,v,w,n,m,tot=0;
double x[N],y[N];
double ans[N];
//有S颗卫星和P个哨所,有卫星的两个哨所之间可以任意通信;
//否则,一个哨所只能和距离它小于等于D的哨所通信。给出卫星的数量和P个哨所的坐标,求D的最小值

void prime(int pos)
{
	for(int i=1;i<=m;i++)
		dist[i]=a[pos][i];
	dist[pos]=0;
	vis[pos]=1;
	for(int i=1;i<m;i++)
	{
		int cur = -1;
		for(int j=1;j<=m;j++)
			if(!vis[j]&&(cur==-1||dist[j]<dist[cur])) cur = j;
//	if(dist[cur]>=INF) return INF;
//	sum += dist[cur];
//	ans[++tot]=dist[cur];
	vis[cur]=true;
	for(int k=1;k<=m;k++)
		if(!vis[k]) dist[k] = min(dist[k],a[cur][k]);
	}
//	sort(dist+1,dist+1+m);
//	printf("%.2f\n",dist[m-n+1]);
//	return sum;
}

signed main()
{
	int T;cin>>T;
	while(T--){
		cin>>n>>m;
		memset(a, 0x3f, sizeof(a));
//		memset(dist, 0x3f, sizeof(dist));
		memset(vis, false, sizeof(vis));
		for(int i=1;i<=m;i++)
			{
				cin>>x[i]>>y[i];
				for(int j=1;j<i;j++)
					a[i][j]=a[j][i]=sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
			}
		prime(1);
		sort(dist+1,dist+1+m);
		printf("%.2f\n",dist[m-n+1]);
	}
	return 0;
}

写在最后:模版

prim模版:

题意:

n个点,m条边,每次给出u,v,w;

u,v表示两点,w表示边长(花的钱),或者说权重

代码:

#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
const int INF = 0x3f3f3f3f3f;
const int N = 505;
int a[N][N],dist[N];
bool vis[N];//用来判断走没走过
int n,m,sum=0;
int u,v,w;

int prime(int pos)
{
	dist[pos]=0;
	for(int i=1;i<=n;i++)
	{
		int cur = -1;
		for(int j=1;j<=n;j++)
			if(!vis[j]&&(cur==-1||dist[j]<dist[cur])) cur = j;
	if(dist[cur]>=INF) return INF;
	sum += dist[cur];
	vis[cur]=true;
	for(int k=1;k<=n;k++)
		if(!vis[k]) dist[k] = min(dist[k],a[cur][k]);
	}
	return sum;
}

signed main()
{
	cin>>n>>m;
	memset(a, 0x3f, sizeof(a));
	memset(dist, 0x3f, sizeof(dist));
	memset(vis, false, sizeof(vis));
	for(int i=1;i<=m;i++)
	{
		cin>>u>>v>>w;
		a[u][v]=min(a[u][v],w);
		a[v][u]=min(a[v][u],w);
	}
	int val = prime(1);
	if(val>=INF) puts("impossible");
	else cout<<sum<<endl;
	return 0;
}

Kruskal模版:

题意:

n个点,m条边,每次给出u,v,w;

u,v表示两点,w表示边长(花的钱),或者说权重

代码:

#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
const int N = 2e5 + 10; 
//kruskal(克鲁斯卡尔算法)的思路就是把所有边的值取出,然后从小到大去取边
//如果达到了n-1就断开,如果未达到,就继续
//如果形成了自环,就打开。

/*用来存图*/
struct node{
	int x,y,z;
}edge[N];

bool cmp(struct node a,struct node b)
{
	return a.z<b.z;
}

int fa[N],n,m,sum;

//求父亲节点
int get(int x)
{
	return x == fa[x] ? x : fa[x] = get(fa[x]);
}

signed main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
		cin>>edge[i].x>>edge[i].y>>edge[i].z;
	for(int i=1;i<=n;i++)
		fa[i]=i;
	sort(edge+1,edge+1+n,cmp);
	for(int i=1;i<=m;i++)
	{
		int x = get(edge[i].x);
		int y = get(edge[i].y);
		if(x==y) continue;
		fa[y] = x;
		sum += edge[i].z;
	}
	int ans = 0;
	for(int i=1;i<=n;i++)
		if(i == fa[i]) ans++;
	if(ans>1) puts("impossible");
	else cout<<sum<<endl;
	return 0;
}

参考文章:

树与树算法(一)树的介绍_BlazarBruce的博客-CSDN博客

最小生成树问题(Prim算法和Kruskal算法的异同总结)_舔狗之王的博客-CSDN博客_用prim和kruskal算法求最小生成树一样吗