最小生成树(Minimum Spanning Tree,MST)是一个无向加权连通图的子图,它包含图中所有顶点,并且所有边的权值之和最小。常见的求解最小生成树的算法有克鲁斯卡尔(Kruskal)算法和普里姆(Prim)算法,下面为你详细介绍这两种算法。
克鲁斯卡尔(Kruskal)算法
算法思路
Kruskal 算法基于贪心策略,其核心思想是先将图中所有边按照权值从小到大进行排序,然后依次选取权值最小的边,如果这条边加入到当前的生成树中不会形成环,就将其加入;重复这个过程,直到生成树包含图中的所有顶点。
算法步骤
- 边排序:将图中所有边按照权值从小到大进行排序。
- 初始化并查集:将每个顶点看作一个独立的集合,即每个顶点的父节点是它自己。
- 遍历边:依次选取权值最小的边,检查这条边的两个端点是否属于不同的集合。如果是,则将这条边加入生成树,并合并这两个集合;否则,跳过这条边。
- 终止条件:当生成树中包含的边数等于顶点数减 1 时,算法结束。
代码实现(C 语言)
#include <stdio.h>
#include <stdlib.h>
#define MAX_EDGES 10000
#define MAX_VERTICES 1000
// 边的结构体
typedef struct {
int u, v; // 边的两个端点
int weight; // 边的权值
} Edge;
// 比较函数,用于 qsort 对边按权值排序
int compare(const void *a, const void *b) {
Edge *edgeA = (Edge *)a;
Edge *edgeB = (Edge *)b;
return edgeA->weight - edgeB->weight;
}
// 并查集查找函数,带路径压缩
int find(int parent[], int i) {
if (parent[i] == i)
return i;
return parent[i] = find(parent, parent[i]);
}
// 并查集合并函数
void unionSets(int parent[], int x, int y) {
int xroot = find(parent, x);
int yroot = find(parent, y);
parent[xroot] = yroot;
}
// Kruskal 算法
int kruskal(Edge edges[], int numEdges, int numVertices) {
qsort(edges, numEdges, sizeof(Edge), compare); // 对边按权值排序
int *parent = (int *)malloc(numVertices * sizeof(int));
for (int i = 0; i < numVertices; i++) {
parent[i] = i; // 初始化并查集
}
int mstWeight = 0; // 最小生成树的总权值
int edgeCount = 0; // 已加入最小生成树的边数
for (int i = 0; i < numEdges && edgeCount < numVertices - 1; i++) {
int u = edges[i].u;
int v = edges[i].v;
int weight = edges[i].weight;
int setU = find(parent, u);
int setV = find(parent, v);
if (setU != setV) {
mstWeight += weight;
edgeCount++;
unionSets(parent, setU, setV);
}
}
free(parent);
return mstWeight;
}
int main() {
int numVertices = 4;
int numEdges = 5;
Edge edges[MAX_EDGES] = {
{0, 1, 10},
{0, 2, 6},
{0, 3, 5},
{1, 3, 15},
{2, 3, 4}
};
int mstWeight = kruskal(edges, numEdges, numVertices);
printf("最小生成树的总权值: %d\n", mstWeight);
return 0;
}
普里姆(Prim)算法
算法思路
Prim 算法同样基于贪心策略,它从一个任意顶点开始,逐步扩展生成树,每次选择与当前生成树相连的权值最小的边,将其另一端的顶点加入生成树,直到生成树包含图中的所有顶点。
算法步骤
- 初始化:选择一个任意顶点作为起始顶点,将其加入生成树。
- 扩展生成树:每次从与当前生成树相连的边中选择权值最小的边,将其另一端的顶点加入生成树。
- 重复步骤 2:直到生成树包含图中的所有顶点。
代码实现(C 语言)
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define MAX_VERTICES 1000
// 找到距离生成树最近的顶点
int minKey(int key[], int mstSet[], int numVertices) {
int min = INT_MAX, min_index;
for (int v = 0; v < numVertices; v++) {
if (mstSet[v] == 0 && key[v] < min) {
min = key[v];
min_index = v;
}
}
return min_index;
}
// Prim 算法
int prim(int graph[MAX_VERTICES][MAX_VERTICES], int numVertices) {
int key[MAX_VERTICES]; // 存储每个顶点到生成树的最小权值
int mstSet[MAX_VERTICES]; // 标记顶点是否已加入生成树
int parent[MAX_VERTICES]; // 存储每个顶点在生成树中的父节点
// 初始化
for (int i = 0; i < numVertices; i++) {
key[i] = INT_MAX;
mstSet[i] = 0;
}
key[0] = 0; // 从顶点 0 开始
parent[0] = -1; // 顶点 0 没有父节点
for (int count = 0; count < numVertices - 1; count++) {
int u = minKey(key, mstSet, numVertices);
mstSet[u] = 1;
for (int v = 0; v < numVertices; v++) {
if (graph[u][v] && mstSet[v] == 0 && graph[u][v] < key[v]) {
parent[v] = u;
key[v] = graph[u][v];
}
}
}
int mstWeight = 0;
for (int i = 1; i < numVertices; i++) {
mstWeight += graph[i][parent[i]];
}
return mstWeight;
}
int main() {
int numVertices = 4;
int graph[MAX_VERTICES][MAX_VERTICES] = {
{0, 10, 6, 5},
{10, 0, 0, 15},
{6, 0, 0, 4},
{5, 15, 4, 0}
};
int mstWeight = prim(graph, numVertices);
printf("最小生成树的总权值: %d\n", mstWeight);
return 0;
}
两种算法的比较
- 适用场景:Kruskal 算法适用于边稀疏的图,因为它主要对边进行操作;Prim 算法适用于边稠密的图,因为它主要对顶点进行操作。
- 实现难度:Kruskal 算法需要使用并查集,实现相对复杂一些;Prim 算法的实现相对简单。
最短路径问题是图论中的经典问题,旨在寻找图中两个顶点之间的最短路径。下面为你详细介绍几种常见的最短路径算法:
1. 迪杰斯特拉(Dijkstra)算法
算法思路
Dijkstra 算法用于求解单源最短路径问题,即从一个给定的源顶点到图中其他所有顶点的最短路径。该算法基于贪心策略,每次从未确定最短路径的顶点中选择距离源顶点最近的顶点,然后以该顶点为中间点,更新其他顶点到源顶点的距离,直到所有顶点的最短路径都被确定。
算法步骤
- 初始化:将源顶点的距离设为 0,其他顶点的距离设为无穷大;标记所有顶点为未确定最短路径。
- 选择顶点:从未确定最短路径的顶点中选择距离源顶点最近的顶点。
- 更新距离:以该顶点为中间点,更新其相邻顶点到源顶点的距离。
- 标记顶点:标记该顶点为已确定最短路径。
- 重复步骤 2 - 4:直到所有顶点的最短路径都被确定。
代码实现(C 语言)
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define MAX_VERTICES 100
// 找到距离源顶点最近且未确定最短路径的顶点
int minDistance(int dist[], int sptSet[], int numVertices) {
int min = INT_MAX, min_index;
for (int v = 0; v < numVertices; v++) {
if (sptSet[v] == 0 && dist[v] <= min) {
min = dist[v];
min_index = v;
}
}
return min_index;
}
// 迪杰斯特拉算法
void dijkstra(int graph[MAX_VERTICES][MAX_VERTICES], int numVertices, int src) {
int dist[MAX_VERTICES]; // 存储每个顶点到源顶点的最短距离
int sptSet[MAX_VERTICES]; // 标记顶点是否已确定最短路径
// 初始化
for (int i = 0; i < numVertices; i++) {
dist[i] = INT_MAX;
sptSet[i] = 0;
}
dist[src] = 0; // 源顶点到自身的距离为 0
for (int count = 0; count < numVertices - 1; count++) {
int u = minDistance(dist, sptSet, numVertices);
sptSet[u] = 1;
for (int v = 0; v < numVertices; v++) {
if (!sptSet[v] && graph[u][v] && dist[u] != INT_MAX && dist[u] + graph[u][v] < dist[v]) {
dist[v] = dist[u] + graph[u][v];
}
}
}
// 输出结果
printf("顶点到源顶点的最短距离:\n");
for (int i = 0; i < numVertices; i++) {
printf("顶点 %d: %d\n", i, dist[i]);
}
}
int main() {
int numVertices = 5;
int graph[MAX_VERTICES][MAX_VERTICES] = {
{0, 4, 0, 0, 0},
{4, 0, 8, 0, 0},
{0, 8, 0, 7, 0},
{0, 0, 7, 0, 9},
{0, 0, 0, 9, 0}
};
int src = 0;
dijkstra(graph, numVertices, src);
return 0;
}
2. 贝尔曼 - 福特(Bellman - Ford)算法
算法思路
Bellman - Ford 算法同样用于求解单源最短路径问题,但它可以处理带有负权边的图。该算法通过对所有边进行 次松弛操作,逐步更新每个顶点到源顶点的最短距离,最后再进行一次松弛操作,检查是否存在负权回路。
算法步骤
- 初始化:将源顶点的距离设为 0,其他顶点的距离设为无穷大。
- 松弛操作:对所有边进行V-1 次松弛操作,即对于每条边(u,v),如果 dist[u]+weight(u,v)<dist[v],则更新 dist[v]为dist[u]+weight(u,v) 。
- 检查负权回路:再进行一次松弛操作,如果还能更新某个顶点的距离,则说明图中存在负权回路。
代码实现(C 语言)
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define MAX_VERTICES 100
#define MAX_EDGES 100
// 边的结构体
typedef struct {
int src, dest, weight;
} Edge;
// 贝尔曼 - 福特算法
int bellmanFord(Edge edges[], int numVertices, int numEdges, int src) {
int dist[MAX_VERTICES];
// 初始化
for (int i = 0; i < numVertices; i++) {
dist[i] = INT_MAX;
}
dist[src] = 0;
// 进行 V - 1 次松弛操作
for (int i = 0; i < numVertices - 1; i++) {
for (int j = 0; j < numEdges; j++) {
int u = edges[j].src;
int v = edges[j].dest;
int weight = edges[j].weight;
if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
}
}
}
// 检查负权回路
for (int j = 0; j < numEdges; j++) {
int u = edges[j].src;
int v = edges[j].dest;
int weight = edges[j].weight;
if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
printf("图中存在负权回路\n");
return 0;
}
}
// 输出结果
printf("顶点到源顶点的最短距离:\n");
for (int i = 0; i < numVertices; i++) {
printf("顶点 %d: %d\n", i, dist[i]);
}
return 1;
}
int main() {
int numVertices = 5;
int numEdges = 8;
Edge edges[MAX_EDGES] = {
{0, 1, -1},
{0, 2, 4},
{1, 2, 3},
{1, 3, 2},
{1, 4, 2},
{3, 2, 5},
{3, 1, 1},
{4, 3, -3}
};
int src = 0;
bellmanFord(edges, numVertices, numEdges, src);
return 0;
}
3. 弗洛伊德(Floyd)算法
算法思路
Floyd 算法用于求解图中任意两个顶点之间的最短路径,它通过动态规划的思想,逐步考虑每个顶点作为中间点,更新任意两个顶点之间的最短距离。
算法步骤
- 初始化:将图的邻接矩阵作为初始的最短距离矩阵。
- 动态规划:对于每个顶点k ,考虑以 k为中间点,更新任意两个顶点 i和 j之间的最短距离,即如果 dist[i][k]+dist[k][j]<dist[i][j],则更新dist[i][j]为dist[i][k]+dist[k][j] 。
- 重复步骤 2:对所有顶点进行上述操作。
代码实现(C 语言)
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define MAX_VERTICES 100
// 弗洛伊德算法
void floydWarshall(int graph[MAX_VERTICES][MAX_VERTICES], int numVertices) {
int dist[MAX_VERTICES][MAX_VERTICES];
// 初始化
for (int i = 0; i < numVertices; i++) {
for (int j = 0; j < numVertices; j++) {
dist[i][j] = graph[i][j];
}
}
// 动态规划
for (int k = 0; k < numVertices; k++) {
for (int i = 0; i < numVertices; i++) {
for (int j = 0; j < numVertices; j++) {
if (dist[i][k] != INT_MAX && dist[k][j] != INT_MAX && dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
// 输出结果
printf("任意两个顶点之间的最短距离:\n");
for (int i = 0; i < numVertices; i++) {
for (int j = 0; j < numVertices; j++) {
if (dist[i][j] == INT_MAX) {
printf("INF ");
} else {
printf("%d ", dist[i][j]);
}
}
printf("\n");
}
}
int main() {
int numVertices = 4;
int graph[MAX_VERTICES][MAX_VERTICES] = {
{0, 5, INT_MAX, 10},
{INT_MAX, 0, 3, INT_MAX},
{INT_MAX, INT_MAX, 0, 1},
{INT_MAX, INT_MAX, INT_MAX, 0}
};
floydWarshall(graph, numVertices);
return 0;
}
总结
- Dijkstra 算法:适用于边权为非负的单源最短路径问题,效率较高。
- Bellman - Ford 算法:可以处理带有负权边的单源最短路径问题,但效率较低。
- Floyd 算法:适用于求解任意两个顶点之间的最短路径问题,代码简单,但时间复杂度较高。