最小生成树
前言
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
若再从剩余的 \(m-k\) 条边中选 \(n-1-k\) 条添加到生成森林中,使其成为G的生成树,并且选出的边的权值之和最小,则这生成树一定包含着 \(n-k\) 条边中连接生成森林的两个不连通节点的权值最小的边
——摘自李煜东《算法竞赛进阶指南》
- 知识理解
通俗来讲,我们将一张边带权的无向图G拆分重组为一棵树。这棵树可以有很多形态,这样的树统称为G的生成树
而最小生成树则是所有生成树中权值之和最小的那棵
- 算法区别
Kruskal算法更趋向于处理边的最小生成树问题
Prim算法更趋向于处理点的最小生成树问题
所以在稠密图中,尤其是完全图的最小生成树求解通常使用Prim
而在稀疏图中求解最小生成树,通常使用Kruskal
Kruskal算法
Kruskal算法就是基于上述推论的:Kruskal算法总是维护无向图的最小生成森林
- 主要思想
最初,可以认为生成森林由零条边构成,每个节点各自构成一棵仅包含一个点的树
在任意时刻,Kruskal算法从剩余的边中选出一条权值最小的,并且这条边的两个端点属于生成森林中两棵不同的树(不连通),把该边加入生成森林
图中节点的连通情况可以用并查集维护
- 算法框架
-
建立并查集,每个点各自构成一个集合
-
把所有边按照权值从小到大排序,一次扫描每条边(x,y,z)
-
若x、y属于同一集合(说明连通)不管,继续扫描下一条边
-
若不在一个集合则合并x、y所在的集合,并把z累加到答案中
-
所有边扫描完成后,第4步中处理过的边就构成了最小生成树
- 时间复杂度
Kruskal算法是针对边进行处理,所以时间复杂度为 \(O(m log m)\)
- 练习题
-
洛谷P1546 [USACO3.1]最短网络 Agri-Net (难度普及/提高-)
-
洛谷P2330 [SCOI2005]繁忙的都市 (难度普及/提高-)
-
洛谷P2504 [HAOI2006]聪明的猴子 (难度普及/提高-)
-
洛谷P2872 [USACO07DEC]Building Roads S (难度普及/提高-)
-
洛谷P1991 无线通讯网 (难度普及+/提高)
-
洛谷P1265 公路修建 (难度普及+/提高)
-
洛谷P4208 [JSOI2008]最小生成树计数 (难度提高+/省选-)
- 练习题思路
洛谷上也有一道【普及-】的最小生成树的模板,但是我觉得那个不必要专门写出来,因为上面几道【普及/提高-】的题相当于模板了qvq
- 洛谷P1546 [USACO3.1]最短网络 Agri-Net
简直是赤裸裸的板子题啊!(只不过加了个题目背景)
值得一提的是这道题的矩阵输入,我们能够发现是对称的,所以存数据的时候存一半就好了!这样的矩阵对称输入还是挺常见的,希望大家掌握(很简单)
直接把这题当模板吧,给出完整代码:
#include
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.vi) { //矩阵对称输入存一半
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;
}
- 洛谷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;
}
- 洛谷P2504 [HAOI2006]聪明的猴子
这道题相比前两道,稍微需要转换一下(就不给出代码了,应该看完思路就会了qwq)
区别:
其他题是直接给出两个边之间道路的长度或代价
而这道题给出的只有点的坐标和每个猴子跳跃的最大距离
要求求出有多少猴子能够一次性跳完所有点
转换:
虽然只给出了每个点的坐标,但是我们可以通过点的坐标计算出每两个点之间的距离啊!
然后再根据这些距离求最小生成树,当然模板中的累加操作也应该直接换成更新操作,将最终答案设为maxn
最后将每个猴子的最大跳跃距离与maxn比较,就可以得出答案
- 洛谷P2872 [USACO07DEC]Building Roads S
这道题相当于上一道题的变式:还是给出点的坐标,但是多给出了m条已经连通的道路
这就与直接构造最小生成树有一点细微的区别了:最小生成树构造时终止条件不再是 \(n-1\) 了,而是 \(n-m-1\)
还有想要提醒一下的就是在求点之间的距离时,如果输入的类型是整型,那么需要人为的强制转换一下,不然会WA掉四个点(大雾...以后注意一下就是)
- 洛谷P1991 无线通讯网
这题我认为是比较难的,思路更是巧妙
这里就直接给出这道题的题解,自认为还是很详细,可以跳转阅读
- 洛谷P1265 公路修建
这道题我用Kruskal只得到了60pts,也不知道怎么优化,大概就是算法错了吧
看了想了一下,针对于点的最小生成树,还是Prim算法更快捷,于是开始敲Prim的代码,然后才A掉
就不给出60pts的代码了,直接挂满分的Prim算法代码
#include
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]