图论 prim算法和kruskal算法

在介绍这两个算法之前,需要先介绍一些图的连通性问题

图分为有向图无向图

由于上述算法都是针对无向图而言,于是下面只介绍无向图的相关概念

对于无向图的两个顶点v,w 如果v,w之间有路径存在,我们称v和w是连通的,如果对于图中任意的两个顶点,这两个顶点均是 连通的,记这张图为G,我们称G是连通的,无向图的极大连通子图成为连通分量(这个概念很重要,后面还会用到)注意:由于图可以没有边,但是必须要有顶点,于是只有一个点而没有其他任何边的子图也称为连通分量。如果一个图是连通图,顶点个数为n,则必须有n-1条边 (这个概念也很重要,后面也会用到)

带权图:如果我们对于连通的图上的每条边赋予具有一定意义的数值,构成的图成为图,带权图的边成为带权边。

最小生成树(MST):

一个连通图的生成树包含图的所有顶点,且包括尽可能少的边,对于生成树来说,如果砍掉其中一条边,则会使得生成树变成非连通图,如果加上一条边,则会形成图中的一条环路。

最小生成树的相关性质:

1.若图G中存在权值相同的边,则G的最小生成树可能不唯一,即最小生成树的树形不唯一。当图G中各边权值均不相等时,G的最小生成树是唯一的(这可以在prim算法和kruskal算法的运行过程中感受到);若无向连通图的结点个数为n,且边数为n-1时,这个图的最小生成树即为这个图本身

2。对于一个带权图来说,最小生成树可能不唯一,但是权和必定唯一且为对最小,因此最小生成树也称为最小代价树。

3.最小生成树的边数为顶点数-1。

下面首先介绍prim算法

prim算法又成为加点法,在手动模拟的时候,我们会选择一个初始结点,一般我们选择编号为0的结点加入生成树中,此时生成树中只有这个结点,标记这个结点后我们找到和这个结点连通的其他结点,同时找到边权最小的结点,将其加入最小生成树中,同样需要标记这个结点,然后我们重复上述操作,直到所有结点都被加入到生成树中,这样得到的这颗生成树就是最小生成树,计入结果的边权和就是prim算法的结果

下面给出对于邻接矩阵,prim算法的实现过程

/* 看博客的时候发现好多博主没有说明白prim算法,只是放了几张图,说了句贪心算法然后扔了一串代码就草草结束了
 * 我这里给出代码和邻接矩阵间的转化关系,希望能够讲明白
 * prim算法和krustal算法不同,prim算法是在图中加入结点,使得结点和最后加入的结点之间的边权最小,
 * 从而得到一个最小生成树,生成树中所有边权之和即为算法得到的最小代价和
 * 而邻接矩阵,代码,手画的图之间是怎么联系起来的呢?
 * 我们知道,对于邻接矩阵g[i][j]代表的是结点i和结点j之间的边权
 * 如果想知道初始加入的0号结点和其他结点的边权应该再怎么看呢?
 * 当然是对应看第0行,随后从左到右观察数值,即为0号元素和其他编号结点之间的距离
 * 根据我上述所说的算法思想
 * 我们需要先找到0号结点所连接的所有结点的最小边权所对应的结点,在下方代码中我们用t保存
 * 同时在标记数组st中,标记st[t] = true 代表已经访问过了
 * 然后我们找到与初始结点0边权最小的结点t后,我们需要更新dist数组
 * 此时dist数组需要保存的是与结点t相连接的所有结点的边权,下方代码会给出是如何更新的
 * 随后重复上述两步操作,知道标记数组st中所有值都为true结束算法
 */
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int INF = 1e9;
const int MAXN = 100;

int g[MAXN][MAXN];
int dist[MAXN];
bool st[MAXN];

int prim(int n) {
    fill(dist,dist+MAXN,INF);
    memset(st,false,sizeof(st));
    int res = 0;
    dist[0] = 0;
    for (int i = 0; i < n; ++i) {
        dist[i] = min(dist[i],g[0][i]);
    }
    for (int i = 0; i < n; ++i) {
        int t = -1,temp = INF;
        for (int j = 0; j < n; ++j) {
            if (!st[j] && dist[j] < temp) {
                t = j;
                temp = dist[j];
            }
        }
        if (t == -1) {return -1;} // 无法构成最小生成树
        st[t] = true;
        res += dist[t];
        for (int i = 0; i < n; ++i) {dist[i] = min(dist[i],g[t][i]);} // 更新可以连通的其他结点
    }
    return res;
}

int main() {
    int n;
    scanf("%d",&n);
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            scanf("%d",&g[i][j]);
        }
    }
    int res = prim(n);
    printf("%d",res);
}

https://www.bilibili.com/video/BV1GL8XeBEQz/?spm_id_from=333.999.0.0

具体有一道例子可以看这个视频,写的时候很草率报错了好几次哈哈

 可以发现对于prim算法来说,时间复杂度和边数|e|无关而和顶点数|v|有关,时间复杂度为O(v^{2})

因此prim算法适用于边的稠密图。

下面介绍kruskal算法:

kruskal算法又称为加边法,在手动模拟的过程中,我们会优先选择边权小的边加入集合,然后将这条边的两个点纳入共同的集合中,重复这样的操作,直到将所有边权较小的边选出,同时所有点位于一个集合中结束,此时的生成树为最小生成树,在这个过程中,如果我们想要实现这样的代码,我们需要用到一个特殊的数据结构,并查集,这个集合只有两个基础功能,合并两个结点和查询输入结点的祖先结点,某两个结点是否已经在同一个集合中

下面给出kruskal算法的实现过程

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;

struct Edge{
    int a,b,w;
    Edge(int _a,int _b,int _w) {
        a = _a,b = _b,w = _w;
    }
};

const int MAXN = 1e5; // 代表结点个数的最大值
int parent[MAXN];     // 存放每个结点的祖先结点
vector<Edge> edges;   // 后续需要排序,方便每次取权值最小的带权边

int find(int x) {
    if (x != parent[x]) {parent[x] = find(parent[x]);} // 递归实现查询祖先结点;
    return parent[x];
}

bool compare(Edge e1, Edge e2) {
    return e1.w < e2.w; 
} // 按照权值排序

int kruskal(int n,int m) {
    for (int i = 0; i < n; ++i) {parent[i] = i;} // 初始每个结点都是连通分量
    sort(edges.begin(),edges.end(),compare);
    int res = 0;
    int cnt = 0;
    for (int i = 0; i < m; ++i) {  // 选边加边的过程
        int a = edges[i].a,b = edges[i].b,w = edges[i].w;
        int x = find(a),y = find(b);
        if (x != y) {
            res += w;
            parent[x] = y;
            cnt++; 
        }
    }
/* 由于连通图的边数必定大于等于n-1,如果加边结束后边数少于结点数-1,说明该图本身为非连通图,无最小生成树
*/
    if (cnt < n - 1) {return -1;} 
    return res;
}

int main() {
    int n,m;
    scanf("%d%d",&n,&m);
    int a,b,w;
    for (int i = 0; i < m; ++i) {
        scanf("%d%d%d",&a,&b,&w);
        edges.push_back(Edge(a,b,w));
    }
    int res = kruskal(n,m);
    printf("%d",res);
    return 0;
}


可以发现,kruscal算法的时间复杂度和边数|e|有关,时间复杂度为O|e|\log |e|,所以kruskal算法适用于边比较稀疏的图

这是我在学习无向图是学习的两种算法的总结,如果哪里出现了错误,欢迎大佬指正,谢谢大家看到这里!!!

  • 18
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值