在介绍这两个算法之前,需要先介绍一些图的连通性问题
图分为有向图和无向图
由于上述算法都是针对无向图而言,于是下面只介绍无向图的相关概念
对于无向图的两个顶点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|有关,时间复杂度为
因此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|有关,时间复杂度为,所以kruskal算法适用于边比较稀疏的图
这是我在学习无向图是学习的两种算法的总结,如果哪里出现了错误,欢迎大佬指正,谢谢大家看到这里!!!