zoukankan      html  css  js  c++  java
  • 最小生成树两种算法详解

    最小生成树

    众所周知, 树是一种特殊的图, 是由n-1条边连通n个节点的图.

    如果在一个有n个节点的无向图中, 选择n-1条边, 将n个点连成一棵树, 那么这棵树就是这个图的一个生成树.

    如果保证树的边权和最小, 那么这棵树就是图的最小生成树.

    为了求一棵树的最小生成树, 有两种算法, 一种是选择点加入树的Prim算法, 另一种是选择边加入树的Kruskal算法.

    Prim算法

    这个算法的过程和Dijkstra类似, 但有所不同.

    首先选择任意一点作为树的第一个节点0, 枚举与它相连的所有点i, 将两点之间的边权记为这个点到生成树的距离b[i], 选择距离最近的点加入生成树, 然后枚举与之相邻的节点j, 用边权a[i,j]更新b[j], 使其等于min(b[j],a[i,j]), 这样再继续加入当前离生成树最近的点, 在更新它相邻的点, 以此类推, 直到所有点全部加入生成树. 这样, 便求出了最小生成树.

    关于正确性

    我自己的思路是这样的: 如果用Prim算法求出了一棵最小生成树, 将一条边u换成另一条更小的v, 就得到一棵边权和更小的生成树. 首先保证树连通, 所以去掉u和v, 生成树被分成两个连通块是一模一样的. 在当时连接u的时候, 已经决策完的生成树一定也和v相连, 这时v连接的节点一定会比u连接的节点更早加入, 所以一开始的假设不成立, 算法正确.

    具体代码实现

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    using namespace std;
    int n,m,l,r,x,a[5005][5005]/*邻接矩阵*/,b[5005]/*点到生成树的最短边权*/,now/*当前加入的点*/,k=1/*生成树节点数*/,ans=0/*生成树总边权和*/;
    bool vsd[5005]={0};
    void update(int at){//用节点at更新其他点的b[]值
    	for(int i=1;i<=n;i++) {
    		b[i]=min(a[at][i],b[i]);
    	}
    	vsd[at]=true;
    	return;
    }
    int find(){//寻找当前离生成树最近的点
    	int ft=0;
    	for(int i=1;i<=n;i++){
    		if(!vsd[i]){//不在树中
    			if(b[i]<=b[ft]){
    				ft=i;
    			}
    		}
    	}
    	return ft;
    }
    int main(){
    	cin>>n>>m;
    	memset(a,0x3f,sizeof(a));
    	for(int i=1;i<=n;i++){
    		a[i][i]=0;
    	}
    	for(int i=1;i<=m;i++){
    		cin>>l>>r>>x;
    		a[l][r]=min(a[l][r],x);//防止有两个点之间出现边权不同的几条边
    		a[r][l]=min(a[r][l],x);
    	}
    	memset(b,0x3f,sizeof(b));
    	update(1);
    	while(k<n){//加入n-1个点后返回(第一个点本来就在树中, 无需加入)
    		now=find();//加入最近的点now
    		ans+=b[now];//统计答案
    		update(now);//更新其他点
    		k++;//统计点数
    	}
    	cout<<ans<<endl;
    	return 0;
    }
    

    Kruskal算法

    这个算法和Prim相反, 它是将边记为树上的边, 最终得到一棵最小生成树.

    将所有边按边权排序, 然后将它们从小到大讨论是否加入生成树. 如果该边的两个端点属于同一个连通块, 这时加入该边就会形成环, 不符合树的定义, 所以舍弃. 如果该边两个端点不属于同一个连通块, 那么连接该边, 将两个端点所在连通块连成一个.

    当共加入n-1条边的时候, 就得到了一棵最小生成树.

    对于查找两点是否在同一个连通块中的方法, 我们可以使用并查集来维护点之间的连通关系.

    正确性简易说明

    Kruskal相对来说更好理解, 因为从小到大排序后, 使用被舍弃的边连成环是非法的, 使用排在后面的合法的边替换已经选择的边, 得到的答案不是最优的. 所以Kruskal算法正确.

    代码实现

    #include<cstdio>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    using namespace std;
    int n,m,fa[10005],s,e,l,k=0,ans=0;
    struct side{
    	int le,ri,len;//起点, 终点, 边权
    }a[200005];
    bool cmp(side x,side y){//结构体sort规则
    	return(x.len<y.len);
    }
    int find(int x){//并查集寻找最老祖先
    	if(fa[x]==x){//自己就是当前连通块最老祖先
    		return x;
    	}
    	fa[x]=find(fa[x]);//自己祖先的最老祖先
    	return fa[x];
    }
    int main(){
    	cin>>n>>m;
    	memset(a,0x3f,sizeof(a));
    	for(int i=1;i<=m;i++){
    		cin>>s>>e>>l;
    		a[i].le=s;//结构体存储边
    		a[i].ri=e;
    		a[i].len=l;
    	}
    	sort(a+1,a+m+1,cmp);//按边权升序排列
    	for(int i=1;i<=n;i++){
    		fa[i]=i;//初始化并查集
    	}
    	int i=0;
    	while((k<n-1/*加入了n-1个点跳出*/)&&(i<=m/*枚举完了所有的边跳出*/)){
    		i++;
    		int fa1=find(a[i].le),fa2=find(a[i].ri);//两个端点的最老祖先
    		if(fa1!=fa2){//不在同一连通块
    			ans+=a[i].len;//记录答案
    			fa[fa1]=fa2;//连接连通块
    			k++;//记录边数
    		}
    	}
    	cout<<ans<<endl;
    	return 0;
    }
    

    之前发的是笔记, 现在发的是实战总结

  • 相关阅读:
    Git SVN 版本控制 简介 总结 MD
    shape selector 背景 圆形 矩形 圆环 [MD]
    eclipse library jar包 使用总结 MD
    Visitor 访问者模式 [MD]
    BlazeMeter+Jmeter 搭建接口测试框架
    nGrinder 简易使用教程
    65个面试常见问题技巧回答(绝对实用)
    [面试技巧]16个经典面试问题回答思路
    质量模型测试电梯
    linux apache服务器优化建议整理(很实用)
  • 原文地址:https://www.cnblogs.com/Wild-Donkey/p/12905633.html
Copyright © 2011-2022 走看看