最小生成树

Posted eleven-qian-shan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最小生成树相关的知识,希望对你有一定的参考价值。

最小生成树

前言

emmm...因为Prim学的不是很好(完全不会编题),所以重点讲Kruskal算法,Prim部分可能会咕很久(炖鸽子警告


最小生成树

  • 知识搬运

给定一张边带权的无向图 (G=(V,E),n=|V|,m=|E|) ,由V中全部n个顶点和E中的 (n-1) 条边构成的无向连通子图被称为G的一棵生成树。边的权值之和最小的生成树被称为无向图G的最小生成树(MST)

定理:

任意一棵最小生成树一定包含无向图中权值最小的边

推论:

给定一张无向图 (G=(V,E),n=|V|,m=|E|) ,从E中选出 (k<n-1) 条边构成G的一个生成森林

若再从剩余的 (m-k) 条边中选 (n-1-k) 条添加到生成森林中,使其成为G的生成树,并且选出的边的权值之和最小,则这生成树一定包含着 (n-k) 条边中连接生成森林的两个不连通节点的权值最小的边

					                                                                                                            ——摘自李煜东《算法竞赛进阶指南》
  • 知识理解

通俗来讲,我们将一张边带权的无向图G拆分重组为一棵树。这棵树可以有很多形态,这样的树统称为G的生成树

而最小生成树则是所有生成树中权值之和最小的那棵

  • 算法区别

Kruskal算法更趋向于处理边的最小生成树问题

Prim算法更趋向于处理点的最小生成树问题

所以在稠密图中,尤其是完全图的最小生成树求解通常使用Prim

而在稀疏图中求解最小生成树,通常使用Kruskal


Kruskal算法

Kruskal算法就是基于上述推论的:Kruskal算法总是维护无向图的最小生成森林

  • 主要思想

最初,可以认为生成森林由零条边构成,每个节点各自构成一棵仅包含一个点的树

在任意时刻,Kruskal算法从剩余的边中选出一条权值最小的,并且这条边的两个端点属于生成森林中两棵不同的树(不连通),把该边加入生成森林

图中节点的连通情况可以用并查集维护

  • 算法框架
  1. 建立并查集,每个点各自构成一个集合

  2. 把所有边按照权值从小到大排序,一次扫描每条边(x,y,z)

  3. 若x、y属于同一集合(说明连通)不管,继续扫描下一条边

  4. 若不在一个集合则合并x、y所在的集合,并把z累加到答案中

  5. 所有边扫描完成后,第4步中处理过的边就构成了最小生成树

  • 时间复杂度

Kruskal算法是针对边进行处理,所以时间复杂度为 (O(m log m))

  • 练习题
  1. 洛谷P1546 [USACO3.1]最短网络 Agri-Net (难度普及/提高-)

  2. 洛谷P2330 [SCOI2005]繁忙的都市 (难度普及/提高-)

  3. 洛谷P2504 [HAOI2006]聪明的猴子 (难度普及/提高-)

  4. 洛谷P2872 [USACO07DEC]Building Roads S (难度普及/提高-)

  5. 洛谷P1991 无线通讯网 (难度普及+/提高)

  6. 洛谷P1265 公路修建 (难度普及+/提高)

  7. 洛谷P4208 [JSOI2008]最小生成树计数 (难度提高+/省选-)

  • 练习题思路

洛谷上也有一道【普及-】的最小生成树的模板,但是我觉得那个不必要专门写出来,因为上面几道【普及/提高-】的题相当于模板了qvq

  1. 洛谷P1546 [USACO3.1]最短网络 Agri-Net

简直是赤裸裸的板子题啊!(只不过加了个题目背景)

值得一提的是这道题的矩阵输入,我们能够发现是对称的,所以存数据的时候存一半就好了!这样的矩阵对称输入还是挺常见的,希望大家掌握(很简单)

直接把这题当模板吧,给出完整代码:

#include <bits/stdc++.h>
using namespace std;
int n,w,tot,now,ans,fa[1000001];

struct node {
	int x,y,v;
} a[1000001];

inline bool cmp(node p,node ps) {
	return p.v<ps.v;
}

inline int find_fa(int x) {
	if(fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}

int main() {
	scanf("%d",&n);
	for(register int i=1;i<=n;i++) {
		fa[i]=i;
		for(register int j=1;j<=n;j++) {
			scanf("%d",&w);
			if(j>i) {  //矩阵对称输入存一半
				a[++tot].x=i;
				a[tot].y=j;
				a[tot].v=w;
			}
		}
	}
	sort(a+1,a+1+tot,cmp);  //注意是tot而不是n或m!下同
	for(register int i=1;i<=tot;i++) {  
		int b=find_fa(a[i].x);
		int c=find_fa(a[i].y);
		if(b!=c) {
			now++;
			fa[b]=c;
			ans+=a[i].v;
		}
		if(now==n-1) break;
	}
	printf("%d",ans);
	return 0;
}
  1. 洛谷P2330 [SCOI2005]繁忙的都市

求最大值最小,Kruskal啊

一定会选择 (n-1) 条边,而最后被加进最小生成树的那条边就是分值最大的那条道路

所以只需要将模板中的累加改为直接更新即可:

for(register int i=1;i<=m;i++) {
		int a=find_fa(road[i].x);
		int b=find_fa(road[i].y);
		if(a!=b) {
			now++;
			fa[a]=b;
			ans=road[i].v;  //因为已经从小到大排序,所以最后加入的就是最大值
		}
		if(now==n-1) break;
	}
  1. 洛谷P2504 [HAOI2006]聪明的猴子

这道题相比前两道,稍微需要转换一下(就不给出代码了,应该看完思路就会了qwq)

区别:

其他题是直接给出两个边之间道路的长度或代价

而这道题给出的只有点的坐标和每个猴子跳跃的最大距离

要求求出有多少猴子能够一次性跳完所有点

转换:

虽然只给出了每个点的坐标,但是我们可以通过点的坐标计算出每两个点之间的距离啊!

然后再根据这些距离求最小生成树,当然模板中的累加操作也应该直接换成更新操作,将最终答案设为maxn

最后将每个猴子的最大跳跃距离与maxn比较,就可以得出答案

  1. 洛谷P2872 [USACO07DEC]Building Roads S

这道题相当于上一道题的变式:还是给出点的坐标,但是多给出了m条已经连通的道路

这就与直接构造最小生成树有一点细微的区别了:最小生成树构造时终止条件不再是 (n-1) 了,而是 (n-m-1)

还有想要提醒一下的就是在求点之间的距离时,如果输入的类型是整型,那么需要人为的强制转换一下,不然会WA掉四个点(大雾...以后注意一下就是)

  1. 洛谷P1991 无线通讯网

这题我认为是比较难的,思路更是巧妙

这里就直接给出这道题的题解,自认为还是很详细,可以跳转阅读

  1. 洛谷P1265 公路修建

这道题我用Kruskal只得到了60pts,也不知道怎么优化,大概就是算法错了吧

看了想了一下,针对于点的最小生成树,还是Prim算法更快捷,于是开始敲Prim的代码,然后才A掉

就不给出60pts的代码了,直接挂满分的Prim算法代码

#include <bits/stdc++.h>
using namespace std;
int n,now,vis[5010];
double ans,x[5010],y[5010],dis[5010];

int main() {
	scanf("%d",&n);
	for(register int i=1;i<=n;i++) scanf("%lf%lf",&x[i],&y[i]);
	memset(dis,0x7f,sizeof(dis));
	dis[1]=0;
	for(register int i=1;i<=n;i++) {
		now=0;
		for(register int j=1;j<=n;j++) {
			if(vis[j]==0&&dis[j]<dis[now]) now=j;
		}
		vis[now]=1;
		for(register int j=1;j<=n;j++) {
			double tot=(x[now]-x[j])*(x[now]-x[j])+(y[now]-y[j])*(y[now]-y[j]);
			if(vis[j]==0&&tot<dis[j]) dis[j]=tot;
		}
			
	}
	for(register int i=1;i<=n;i++) ans+=sqrt(dis[i]);
	printf ("%.2lf",ans);
	return 0;
}

To be continued...


以上是关于最小生成树的主要内容,如果未能解决你的问题,请参考以下文章

最小生成树matlab代码Kruskal算法,用于二维网络生成

c语言最小生成树

最小生成树及Prim算法及Kruskal算法的代码实现

数据结构 图连通与最小生成树

次最小生成树 模版

图的最小生成树算法(图解+代码)| 学不会来看我系列