3.29 最小生成树算法

最小生成树是解决带权图最优连接问题的一种方法,例如在构建城市道路网络时降低成本。克鲁斯卡尔算法通过并查集避免环路,按边权值从小到大选择;而普里姆算法从一个点出发逐步扩展,更新集合外点到集合的最短距离。两者都是贪心算法的应用。
摘要由CSDN通过智能技术生成

最小生成树概念

参考:什么是最小生成树?

Minimum Spanning Tree

何为生成树?

生成树是指一个联通图的极小的连通子图,它包含了图中的所有n个顶点,并只有n-1条边(构成一棵树)
在这里插入图片描述
生成树的一些性质:

  • 一个连通图可以有多种生成树(如上图所示)
  • 生成树中不存在环
  • 任何一个边的加入都会使得生成树中出现环
何为最小生成树?

最小生成树是针对一个带权图来说的,就是指原图中能构成的所有生成树中,边权和最小的一颗生成树

对于下面的连通图:
在这里插入图片描述
可以有多种选取边(即构成了生成树的情况):
在这里插入图片描述
我们称权值和最小的树为最小生成树

最小生成树有什么意义呢?

接上图,如果我们需要在某城市之间修建道路,点与点之间的加权边就是两城市道路的维修费,如果我们想找到一种将所有城市都连通起来并且使得道路修建费用最小的一种方案,那么就要选择使用最小生成树。

最小生成树构建方法

两种方法都是基于贪心
有一点,其实我不太明白。“Kruskal算法维护的是边,所以不需要对于无向图存储两次边。Prim维护的是集合之间点的关系,因此需要对无向图存储两次边

克鲁斯卡尔算法(Kruskal)

算法思想

将所有的带权边按照权值从小到大排列,从最小的开始选择,如果加入该边后不会产生环,则加入,反之则不加入。直到已经选择了n-1条边。
这里不会产生环的判定比较困难,因此可以换一下想法:
就是只要选择的边的两端点不在同一连通块中即可。如下所示
在这里插入图片描述
使用并查集就可以检查联通问题了


#include<iostream>
#include<algorithm>

using namespace std;

const int N=2*1e5+10;

int n,m;
int p[N];
int ans=0;//最终权值
int res=0;//已经放入的边数

struct nn{
    int a;
    int b;
    int w;
}node[N];

int cmp(nn a, nn b){
    return a.w<b.w;
}

int find(int x){
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}


int kruskal(){
    
    for(int i=1;i<=n;i++)
        p[i]=i;
    
    for(int i=1;i<=m;i++){
        //遍历m条边
        int a=node[i].a, b=node[i].b, w=node[i].w;
        a=find(a); b=find(b);
        if(a!=b){
            //如果不在一个联通图中
            p[a]=b;//连接两个点
            ans+=w;
            res++;
        }
        if(res==n-1){
            return res;
        }
    }
    return res;
}

int main(){
    
    cin>>n>>m;
    
    for(int i=1;i<=m;i++){
        cin>>node[i].a>>node[i].b>>node[i].w;
    }
    
    sort(node+1,node+1+m,cmp);
    
    if(kruskal()==n-1)
        cout<<ans<<endl;
    else
        cout<<"impossible"<<endl;
    
    return 0;
}

普里姆算法(Prim)

和kruskal算法(遍历边)不同的是,prim算法是对点进行遍历

算法思路

对于每个点,每次找到一个集合外的,距离该集合最近的点t。(该集合是一个已经确定了的连通块)
用点t更新集合外点到集合的距离,就是找集合外和t有连接的点,如果更小就更新,最后将点t放入集合中。(这里描述的不是很清楚)

st[j]==0&&(t==-1||dist[t]>dist[j]) 注意这里的判断,首先判断该点是否已经进入集合,然后再与上(t==-1和大小的或运算)


#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N=510,INF=0x3f3f3f3f;
int n,m;

int g[N][N];//用于存储边
int dist[N];//点距离集合的距离
int st[N];//表示是否已经进入集合
int ans=0;

int prim(){
    
    dist[1]=0;//强制一开始选择的点就是点1
    
    for(int i=1;i<=n;i++){//外层循环是指循环n次以遍历所有n个点
        
        int t=-1;
        for(int j=1;j<=n;j++){
            if(st[j]==0&&(t==-1||dist[t]>dist[j])){
                t=j;
            }  
        }
        //找到最短的dist,下标为t
        
        if(dist[t]==INF)    return INF;//说明没有点能够到达当前的集合
        
        ans+=dist[t];
        
        for(int j=1;j<=n;j++){
            if(st[j]==0&&j!=t){
                //更新所有集合外的点到该集合的距离
                dist[j]=min(dist[j],g[t][j]);
            }
        }
        
        st[t]=1;//将点加入集合
        
    }
    
    
}

int main(){
    
    cin>>n>>m;
    
    memset(g,0x3f,sizeof g);
    
    for(int i=1;i<=m;i++){
        int a,b,c;
        cin>>a>>b>>c;
        g[a][b]=g[b][a]=min(g[a][b],c);
    }
    
    memset(dist,0x3f,sizeof dist);
    
    if(prim()==INF)
        cout<<"impossible"<<endl;
        
    else
        cout<<ans<<endl;
    
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值