• 最小生成树


    最小生成树

    前言

    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...


  • 相关阅读:
    PCL利用RANSAC自行拟合分割平面
    HDU 3062 && HDU 1824 && POJ 3678 && BZOJ 1997 2-SAT
    BZOJ 3670 && BZOJ 3620 && BZOJ 3942 KMP
    BZOJ 1500 Splay 全操作
    Manacher
    POJ 2155 2维线段树 || 2维BIT
    BZOJ 1015 并查集+离线倒序
    NOI Linux JAVA
    UVA 10407 差分思想的运用
    BZOJ 1969 树链剖分+Tarjan缩点
  • 原文地址:https://www.cnblogs.com/Eleven-Qian-Shan/p/13195128.html
Copyright © 2020-2023  润新知