前言
本将介绍最小生成树以及普里姆算法(Prim)和克鲁斯卡尔(Kruskal)
本人其他博客:https://blog.csdn.net/2401_86940607
图的基本概念和存储结构:【数据结构与算法】——图(一)
源代码见gitte: https://gitee.com/mozhengy
最小生成树
- 前言
- 正文
- 1. 生成树与最小生成树
- 1.1 生成树的概念
- 1.2 构造准则
- 1.3 生成树与非连通图
- 2. Prim算法
- 2.1 算法思想
- 2.2 算法步骤
- 2.3 示例图解
- 2.4 代码实现
- 2.5 时间复杂度
- 3. Kruskal算法
- 3.1 算法思想
- 3.2 算法步骤
- 3.3 示例图解
- 3.4 代码实现
- 3.4.1 基础版
- 3.4.2 堆排序和并查集优化版(选择)
- 3.5 时间复杂度
- 4. 算法对比
- 结语
正文
1. 生成树与最小生成树
1.1 生成树的概念
- 定义:连通图的生成树是包含图中所有顶点的极小连通子图,具有以下特性:
- 包含全部 n n n 个顶点和 ( n − 1 ) (n-1) (n−1) 条边
- 添加任意一条边会形成回路
- 边数少于 ( n − 1 ) (n-1) (n−1) 则为非连通图
- 最小生成树 (MST):带权连通图中边权之和最小的生成树。
1.2 构造准则
- 仅使用图中的边
- 恰好使用 ( n − 1 ) (n-1) (n−1) 条边
- 不允许产生回路
1.3 生成树与非连通图
在对无向图进行遍历时,
- 若是连通图,仅需调用遍历过程(DFS或BFS)一次,从图中的任一顶点出发便可以遍历图中的各个顶点;
- 若是非连通图,则需调用遍历过程多次,每次调用得到的顶点集和相关的边一起构成了图的一个连通分量。
由深度优先遍历得到的生成树称为深度优先生成树(DFStree)。在深度优先遍历中如果将每次“前进”(纵向)路过的(将被访问)顶点和边都记录下来,就得到了一个子图,该子图为以出发点为根的树,就是深度优先生成树。相应地,由广度优先遍历得到的生成树称为广度优先生成树(BFS tree)。
这样的生成树由遍历时访问过的n个顶点和遍历时经历的(n一1)条边组成。
对于非连通图,每个连通分量中的顶点集和遍历时走过的边一起构成一颗生成树,各个连通分量的生成树组成非连通图的生成森林
2. Prim算法
2.1 算法思想
从单一顶点逐步扩展生成树,每次选择连接当前生成树与剩余顶点的最小权边。
2.2 算法步骤
- 初始化顶点集合 U = { v } U = \{v\} U={v},候选边为 v v v 到其他顶点的边
- 重复 ( n − 1 ) (n-1) (n−1) 次:
- 从候选边中选择权值最小的边 ( k , j ) (k,j) (k,j),将 k k k 加入 U U U
- 更新候选边:检查 V − U V-U V−U 中顶点到 U U U 的新最小边
简单的说
Prim算法是加边
从起始点开始,每次从候选边中挑选权值最小的边加入生成树
2.3 示例图解
从顶点0出发的Prim算法过程:
带权连通图如下
-
仅留下所有顶点
-
选择与0相连权值最小的(0,5)
-
现在与0和5直接相连的边中(5,4)权值更小选择
-
选择权值更小的(4,3)
-
选择(3,2)
-
选择(2,1)
-
选择(1,6)
2.4 代码实现
图的基本实现见图的基本概念和存储结构:【数据结构与算法】——图(一)
#define MAXV 100
#define INF 0x3f3f3f3fvoid Prim(MatGraph g, int v) {int lowcost[MAXV], closest[MAXV];for (int i=0; i<g.n; i++) {lowcost[i] = g.edges[v][i];closest[i] = v;}lowcost[v] = 0;for (int i=1; i<g.n; i++) {int min = INF, k = -1;for (int j=0; j<g.n; j++) {if (lowcost[j] != 0 && lowcost[j] < min) {min = lowcost[j];k = j;}}printf("边(%d,%d) 权:%d\n", closest[k], k, min);lowcost[k] = 0;for (int j=0; j<g.n; j++) {if (g.edges[k][j] < lowcost[j]) {lowcost[j] = g.edges[k][j];closest[j] = k;}}}
}
2.5 时间复杂度
- 复杂度: O ( n 2 ) O(n^2) O(n2),适合稠密图
3. Kruskal算法
3.1 算法思想
按边权递增顺序选择边,确保不形成回路。
3.2 算法步骤
- 初始化所有顶点为独立连通分量
- 按边权排序
- 依次选择最小边:
- 若边的两个顶点属于不同连通分量,则加入生成树
- 合并两个连通分量
3.3 示例图解
Kruskal过程:以此图为例
(1)按照权值递增排序结果
(2)仅包含所有顶点
(3)选择第1条边
(4)选择第2条边
(5)选择第3条边
(6)选择第4条边
(7)选择第5条边
(8)选择第6条边
3.4 代码实现
3.4.1 基础版
/*--- 原始Kruskal算法(直接插入排序) ---*/
typedef struct {int u; // 边的起始顶点int v; // 边的终止顶点int w; // 边的权值
} Edge;void Kruskal(MatGraph g) {int i, j, u1, v1, sn1, sn2, k;int vset[MAXV];Edge E[MaxSize];k = 0;// 生成边集数组 Efor (i = 0; i < g.n; i++) {for (j = 0; j <= i; j++) { // 仅处理下三角避免重复if (g.edges[i][j] != 0 && g.edges[i][j] != INF) {E[k].u = i;E[k].v = j;E[k].w = g.edges[i][j];k++;}}}InsertSort(E, k); // 对 E 按权值直接插入排序// 初始化顶点集合for (i = 0; i < g.n; i++) vset[i] = i;j = 0; // E 数组下标k = 1; // 已选边数while (k < g.n) { // 需选 n-1 条边u1 = E[j].u;v1 = E[j].v;sn1 = vset[u1];sn2 = vset[v1];if (sn1 != sn2) {printf("(%d,%d):%d\n", u1, v1, E[j].w);k++;// 合并两个集合for (i = 0; i < g.n; i++) {if (vset[i] == sn2) vset[i] = sn1; // 修正赋值操作符}}j++;}
}
3.4.2 堆排序和并查集优化版(选择)
#include "UFSTree.h" // 假设并查集实现已包含void ImprovedKruskal(MatGraph g) {Edge E[MaxSize];UFSTree S[MaxSize];int i, j, k = 0;// 生成边集数组 Efor (i = 0; i < g.n; i++) {for (j = 0; j < i; j++) { // 仅处理下三角if (g.edges[i][j] != 0 && g.edges[i][j] != INF) {E[k].u = i;E[k].v = j;E[k].w = g.edges[i][j];k++;}}}HeapSort(E, k); // 堆排序优化Init(S, g.n); // 并查集初始化int edgeCount = 0; // 已选边数j = 0; // E 数组下标while (edgeCount < g.n - 1) {int u1 = E[j].u;int v1 = E[j].v;int sn1 = Find(S, u1);int sn2 = Find(S, v1);if (sn1 != sn2) {printf("(%d,%d):%d\n", u1, v1, E[j].w);Union(S, u1, v1); // 并查集合并edgeCount++;}j++;}
}
3.5 时间复杂度
- 基础实现: O ( e 2 ) O(e^2) O(e2)
- 优化版(堆排序+并查集): O ( e log e ) O(e \log e) O(eloge),适合稀疏图
4. 算法对比
特性 | Prim算法 | Kruskal算法 |
---|---|---|
适用图类型 | 稠密图 | 稀疏图 |
时间复杂度 | O ( n 2 ) O(n^2) O(n2) | O ( e log e ) O(e \log e) O(eloge) |
存储结构 | 邻接矩阵 | 边集数组 |
思想核心 | 顶点扩展 | 边筛选+并查集 |
结语
这部分是图的重要内容,工科学习中有重要作业,由于未学习离散数学,如有错误还望多多指正,写作耗时还望三连支持