zoukankan      html  css  js  c++  java
  • 浅谈最小生成树

    部分内容摘自 李煜东《算法进阶》(个人认为非常好的一本书)

    一些有关最小生成树的知识

    个人比较蒻,讲得可能不太清楚,建议大家还是翻翻书,查查资料,找一些比较权威的说法。那么接下来就是蒟蒻自己的一些理解了

    最小生成树,就是给你一个无向图,让你选一些边出来,然后让这些边形成的新的图保持连通且边权之和最小。这样来理解可能比较通俗易懂,其实这么看来,让一个图保持连通且权值最小,先不考虑权值,我们最少需要n-1条边让这个图保持连通,此时这就是一个树形结构(可能大概这就是为什么叫最小生成树的原因吧),但是注意最小生成树不能直接跑最短路

    那么如何构造出这一颗最小生成树呢?涉及到两种算法,一个是Prim算法和Kruskal算法

    Kruskal算法

    时间复杂度为O(m log m)

    Kruskal算法总是会去维护无向图中的最小生成森林。最初,森林由零条边构成,每个节点自己构成一棵树

    在生成最小生成树的过程中,会在剩余的边中选一条边权最小的边加入到森林中,且保证这条边连接的两个点不属于同一集合(还没有被放在森林中)。这样的话,我们就可以用并查集来实现这一个过程,不会的并查集的可以去看一下我另一个博客(无耻宣传

    Kruskal算法详细流程:
    1.并查集初始化,每一个节点自己构成一个集合
    2.将所有边按边权从小到大排序,遍历所有边(x,y,z)
    3.若x,y不属于同一集合,那么加入这一条边,并将x,y合并;否则continue
    4.所有的边扫描完之后,最小生成树已经构成 
    
    struct edge{
    	int x,y,z;
    }e[MAXN];
    bool cmp(edge q,edge w){
    	return q.z<w.z;
    } //存边和排序 
    int f[MAXN];
    int find(int x){
    	if(f[x]==x) return x;
    	return f[x]=find(f[x]);
    }
    void merge(int x,int y){
    	f[find(x)]=find(y);
    }//并查集基本操作 
    ···
    ···
    for(register int i=1;i<=n;i++) f[i]=i; //并查集初始化 
    for(register int i=1;i<=m;i++){
    	e[i].x=u,e[i].y=v,e[i].z=w;
    }  //存边 
    sort(e+1,e+1+m,cmp); //排序 
    int now=0,ans=0; //now为建了多少边,ans为权值之和 
    for(register int i=1;i<=m;i++){
    	int u=find(e[i].x),v=find(e[i].y); 
    	if(u==v) continue; //如果在一个集合中就continue 
    	else{ 
    		merge(u,v);
    		now++;
    		ans+=e[i].z;
    	} //合并,边权累加,已连边数++ 
    	if(now==n-1) break; //已经连通就退出 
    }
    

    Prim算法

    时间复杂度为O(n^2),堆优化之后为O(m log n),但其实堆优化之后不如Kruskal方便,所以Prim在处理稠密图和完全图时更加优秀,但是Kruskal还是比较实用的

    Prim算法总是维护最小生成树的一部分,最初,Prim算法只确定1号属于最小生成树。我们设已经确定属于最小生成树的节点集合为T,剩余的节点集合为S。我们找到两个端点x和y,分别属于T和S,且边权最小,将y从S中删去,并加入到集合T中

    我们可以维护一个数组d:对于x ∈ S,d[x]表示节点x与集合T中的节点之间权值最小的边的权值。若x ∈ T,则d[x]就等于x被加入T时选出的最小边的权值

    int a[MAXN][MAXN],d[MAXN],n,m,ans;
    bool v[MAXN];
    
    void prim(){
    	memset(d,0x3f,sizeof d);
    	memset(v,0,sizeof v);
    	d[1]=0; //将1加入进去 
    	for(register int i=1;i<n;i++){
    		int x=0;
    		for(register int j=1;j<=n;j++){
    			if(!v[j]&&(x==0||d[j]<d[x])) x=j; //如果没有加进去过且权值最小 
    		}
    		v[x]=1;
    		for(register int y=1;y<=n;y++){
    			if(!v[y]) d[y]=min(d[y],a[x][y]); //维护d数组 
    		} 
    	}
    } 
    ···
    ···
    memset(a,0x3f,sizeof a);
    for(register int i=1;i<=n;i++) a[i][i]=0;
    for(register int i=1;i<=m;i++){
    	a[x][y]=a[y][x]=min(a[x][y],z);
    } 
    prim(); //求最小生成树 
    for(register int i=2;i<=n;i++) ans+=d[i];
    

    以上大概就是对两种算法的讲解了,(Prim)讲解得不太清楚,可能需要读者自己理解一下(是我太菜。。。)

    P2230 繁忙的都市

    模板题,自己做,完全没有思维难度

    P2504 聪明的猴子

    我们之后的省选是这种难度那多好。

    这道题稍微那么不模板一点,为什么呢?因为它有x轴和y轴,所以我们的边数应该是(n^2),所以加一些处理就好了,还是把代码放出来

    #include <bits/stdc++.h>
    using namespace std;
    double maxn=-20050206.0;
    int n,m,ans,tot,now,x[2000010],y[2000010],a[2000010],fa[2000010],flag[2000010];
    
    struct node {
    	int x,y;
    	double v;
    } e[2000010];
    
    inline bool cmp(node xx,node yy) {
    	return xx.v<yy.v;
    }
    
    inline int find(int x) {
    	if(fa[x]==x) return x;
    	return fa[x]=find(fa[x]);
    }
    
    int main() {
    	scanf("%d",&m);
    	for(register int i=1;i<=m;i++) {
    		scanf("%d",&a[i]);
    	}
    	scanf("%d",&n);
    	for(register int i=1;i<=n;i++) {
    		scanf("%d%d",&x[i],&y[i]);
    	}
    	memset(e,20040915,sizeof e);
    	for(register int i=1;i<=n;i++) {
    		fa[i]=i;
    		for(register int j=1;j<=n;j++) {
    			if(i==j) continue;
    			e[++tot].x=i;
    			e[tot].y=j;
    			e[tot].v=sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
    		}
    	}
    	sort(e+1,e+1+tot,cmp);
    	for(register int i=1;i<=tot;i++) {
    		int b=find(e[i].x);
    		int c=find(e[i].y);
    		if(b!=c) {
    			fa[b]=c;
    			now++;
    			maxn=max(maxn,e[i].v);
    		}
    		if(now==n-1) break;
    	}
    	for(register int i=1;i<=m;i++) {
    		if(a[i]>=maxn) ans++;
    	}
    	printf("%d",ans);
    	return 0;
    }
    
    

    P2212 Watering the Fields S

    上面一道题的一点点变种,只是对于不符合条件的边直接丢旁边不管就行了

    #include<bits/stdc++.h>
    using namespace std;
    const int MAXN=2e7+50;
    int n,c;
    int x[MAXN],y[MAXN];
    struct node {
    	int x,y;
    	int z;
    } e[MAXN];
    bool cmp(node q,node w) {
    	return q.z<w.z;
    }
    int f[MAXN];
    int find(int x) {
    	if(f[x]==x) return x;
    	return f[x]=find(f[x]);
    }
    void merge(int x,int y) {
    	f[find(x)]=find(y);
    }
    int main() {
    	scanf("%d%d",&n,&c);
    	for(register int i=1; i<=n; i++) scanf("%d%d",&x[i],&y[i]),f[i]=i;
    	int tot=0;
    	for(register int i=1; i<=n; i++) {
    		for(register int j=1; j<i; j++) {
    			int w=(x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]);
    			if(w<c) continue;
    			else e[++tot].x=i,e[tot].y=j,e[tot].z=w;
    		}
    	}
    	sort(e+1,e+1+tot,cmp);
    	int now=0,ans=0;
    	for(register int i=1; i<=tot; i++) {
    		int u=find(e[i].x),v=find(e[i].y);
    		if(u!=v) {
    			merge(u,v);
    			ans+=e[i].z;
    			now++;
    		}
    		if(now==n-1) break;
    	}
    	if(now==n-1) cout<<ans;
    	else cout<<-1;
    	return 0;
    }
    

    P1194 买礼物

    这个题很舒服啊,它是一个矩阵,所以它是一个对称的图形,我们的读入只需要读取其中的一半,而剩下的一半再读入就会出现重边的情况。再看这道题的话,记得比较优惠和原价谁更便宜(为什么优惠之后会更贵啊)

    #include <bits/stdc++.h>
    using namespace std;
    int n,m,u,tot,now,ans,fa[500010],flag[500010];
    
    struct node {
    	int x,y,v;
    } e[500010];
    
    inline bool cmp(node xx,node yy) {
    	return xx.v<yy.v;
    }
    
    inline int find(int x) {
    	if(fa[x]==x) return x;
    	return fa[x]=find(fa[x]);
    }
    
    int main() {
    	scanf("%d%d",&n,&m);
    	for(register int i=1;i<=m;i++) {
    		fa[i]=i;
    		for(register int j=1;j<=m;j++) {
    			scanf("%d",&u);
    			if(u!=0&&j>i) {
    				e[++tot].x=i;
    				e[tot].y=j;
    				e[tot].v=u;
    			}
    		}
    	}
    	sort(e+1,e+1+tot,cmp);
    	for(register int i=1;i<=tot;i++) {
    		if(now==m-1) break;
    		int b=find(e[i].x);
    		int c=find(e[i].y);
    		if(b!=c) {	
    			fa[b]=c;
    			now++;
    			ans+=min(e[i].v,n);
    		}
    	}
    	printf("%d",ans+n);
    	return 0;
    }
    
    
  • 相关阅读:
    减少注入元
    浅谈微服务基建的逻辑
    Linux系统Java环境安装配置
    ASP.NET Core 依赖注入
    C#并发编程实例讲解-概述(01)
    Linux系统从安装开始
    关于C#获取动态的时间差函数
    SQL Server 存储过程
    jQuery选择器大全(48个代码片段+21幅图演示)
    对齐方式
  • 原文地址:https://www.cnblogs.com/Poetic-Rain/p/13187860.html
Copyright © 2011-2022 走看看