图论算法-最小生成树

1.概念

对连通图进行遍历,过程中所经过的边和顶点的组合可看做是一棵普通树,通常称为生成树。
连通图中的生成树必须满足以下 2 个条件:

  1. 包含连通图中所有的顶点;
  2. 任意两顶点之间有且仅有一条通路;

所有生成树中权值最小的叫做 最小生成树。
对应地,非连通图中的类似概念为生成森林

2.算法

1.Prim算法

    1.思路:
1.反正都要经过所有点,所以先选定一个起点;
2.设置一个dist数组,表示以i为终点邻接边的最小权值;
3.遍历当前起点的所有邻接边,按2中的含义更新dist;
4.下一次的起点为dist中的最小值对应的终点。当然,只能在没访问过的点中寻找
5.按照如上方式,直至遍历完所有节点为止。

核心代码:

#define long  long long

void add(int st,int end,int w,int m) {
    G[m].next=first[st];
    G[m].to=end;
    G[m].wei=w;
    first[st]=m;
}
//注意:找最小边时必须确保没访问过
int find_min() {
    long minn=INF;
    int mini=n+1;
    for(int i=1; i<=n; i++) {
        if(dist[i]<minn&&!vis[i]) {
            minn=dist[i];
            mini=i;
        }
    }
    return mini;
}

long prim(int st) {
    int tot=1;
    //tot:已经访问结点数
    long res=0;
    vis[st]=1;
    //以上为预处理起点
    while(tot<n) {
        //遍历完所有边,即为找到一棵最小生成树
        st=find_min();
        //查找最小值,这里可以用堆优化
        if(st==n+1)
            return 0;
        res+=dist[st];
        for(int i=first[st]; i!=-1; i=G[i].next) {
            if(dist[G[i].to]>G[i].wei&&!vis[G[i].to]) dist[G[i].to]=G[i].wei;
            //这是与Dijkstra的最大不同,因为要找的是邻接的边
        }
        vis[st]=1;
        tot++;
    }
    return res;
}

Prim算法的核心思想就是从任意结点开始,每次增加一条最小权边构成一棵新树。如果图比较稀疏,那就不是很合适了。

例题链接
思路:直接套用模板即可,注意处理可能存在的自环和重边。
我一开始是用邻接矩阵做的,但效率比较低,关键还浪费空间。后来采用向前星就好了。但在处理自环和重边那里又卡了一下,其实建图时遇到重边无需直接舍弃,在初始化dist数组时动手脚即可,详见代码:

#include <cstdio>
#include <algorithm>
#define long  long long
#define INF 0x3f3f3f3f
const int MAX_NODE=5001,MAX_EDGE=200001;
using namespace std;
struct graph {
    int next,to,wei;
    graph() {next=to=-1,wei=INF;}
} G[MAX_EDGE*2]; //注意是无向边
int first[MAX_NODE];
int n,dist[MAX_NODE];
bool vis[MAX_NODE];
//dist为以i为终点的邻接的最小权值
void add(int st,int end,int w,int m) {
    G[m].next=first[st];
    G[m].to=end;
    G[m].wei=w;
    first[st]=m;
}
//注意:找最小边时必须确保没访问过
int find_min() {
    long minn=INF;
    int mini=n+1;
    for(int i=1; i<=n; i++) {
        if(dist[i]<minn&&!vis[i]) {
            minn=dist[i];
            mini=i;
        }
    }
    return mini;
}
//首先假定一个起点,这里为1
long prim(int st) {
    int tot=1;
    //tot:已经访问结点数
    long res=0;
    vis[st]=1;
    //以上为预处理起点
    while(tot<n) {
        //遍历完所有边,即为找到一棵最小生成树
        st=find_min();
        //查找最小值,这里可以用堆优化
        if(st==n+1)
            return 0;
        res+=dist[st];
        for(int i=first[st]; i!=-1; i=G[i].next) {
            if(dist[G[i].to]>G[i].wei&&!vis[G[i].to]) dist[G[i].to]=G[i].wei;
            //这是与Dijkstra的最大不同,因为
        }
        vis[st]=1;
        tot++;
    }
    return res;
}
//假定起点为1
int main() {
    int m,t=0;
    scanf("%d%d",&n,&m);
    fill(dist+1,dist+n+1,INF);
    fill(first,first+n+1,-1);
    for(int i=0; i<m; i++) {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        if(u!=v) {
            add(u,v,w,t++);
            add(v,u,w,t++);
        }
    }
    for(int i=first[1]; i!=-1; i=G[i].next)    dist[G[i].to]=min(dist[G[i].to],G[i].wei); //可能有重边
    long res=prim(1);
    res!=0?printf("%lld",res):printf("orz");
    return 0;
}

2.Kruskral算法

    1.思路
与Prim算法不同,Kruskral算法采用的是“避圈法”。具体地说,就是将边权排序,每次都取最小的边,如果这个边与已有边构成回路,则舍弃,寻找次小边,以此类推。
不难看出,本算法的难点在于如何判断是否构成回路,可以这样思考:
有一张这样的图
在这里插入图片描述
很显然,四个点已经“关联”在了一起,这时再加入一条边(4,1),则能构成回路。如果将(3,4)这条边去掉,那么4和3显然是没有“关联”的,再加入(3,4)这条边也无法构成回路。
可以得出结论:如果两点之间已形成“关联”,则在它们之间建立一条边就会形成回路。
那怎么判断几个点有无“关联”呢?很显然,我们可以引入一个并查集。

7.2补充:图论中有一个结论:n个结点的连通图中至少有n-1条边,如果此时再引入一条不同的边则必然构成回路。而并查集两个值相同说明两点间已经连通,就不能在两点间建边。这样解释为何要用并查集可能比上面更科学一点。

我们可以直接使用边集数组存图。
核心代码如下:

struct graph {
    int st,end,w;
} G[MAX_EDGE];

int kruskral(int m) {
    int res=0;
    for(int i=0; i<m; i++) {
    //m为边数
        int x=Find(G[i].st),y=Find(G[i].end);
        //Find为并查集查找函数
        if(x!=y) {  //避圈法
            fa[x]=y;  //合并并查集
            res+=G[i].w;
        }
    }
    return res;
}
//建图过程在此不表,注意按照边权大小排序

用这一算法解决上面的问题,效率稍高一些。
由于此算法是按照边查找,所以更适合稀疏图。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值