Dijkstra、Prim、Kruskal和拓扑排序的原理与实现
图算法是计算机科学中的核心内容,广泛应用于网络路由、社交网络分析、任务调度等领域。本文将详细介绍四种经典图算法:Dijkstra最短路径算法、Prim最小生成树算法、Kruskal最小生成树算法和拓扑排序算法,包括它们的原理、应用场景以及完整的C语言实现。
一、Dijkstra最短路径算法
算法原理
Dijkstra算法由荷兰计算机科学家Edsger W. Dijkstra于1956年提出,用于解决单源最短路径问题。算法采用贪心策略,逐步确定从源点到所有其他顶点的最短路径。
核心思想:
-
维护两个集合:已确定最短路径的顶点集合S和未确定的集合Q
-
每次从Q中选出距离源点最近的顶点u,加入S
-
松弛(relax)u的所有邻接顶点v的距离
-
重复直到Q为空
时间复杂度:使用优先队列时为O((V+E)logV),普通实现为O(V²)
应用场景
-
网络路由协议(如OSPF)
-
交通导航系统
-
社交网络中查找关系最近的路径
C语言实现
#include <stdio.h>
#include <limits.h>
#include <stdbool.h>
#define V 6 // 图中顶点的数量
// 找到距离数组中最小距离的顶点的索引
int minDistance(int dist[], bool sptSet[]) {
int min = INT_MAX, min_index;
for (int v = 0; v < V; v++) {
if (!sptSet[v] && dist[v] <= min) {
min = dist[v];
min_index = v;
}
}
return min_index;
}
// 打印最短路径
void printPath(int parent[], int j) {
// 如果j是源节点则停止递归
if (parent[j] == -1)
return;
printPath(parent, parent[j]);
printf("-> %d ", j);
}
// 打印构造的距离数组
void printSolution(int dist[], int parent[], int src) {
printf("顶点\t 距离\t 路径");
for (int i = 0; i < V; i++) {
if (i != src) {
printf("\n%d -> %d \t %d\t %d ", src, i, dist[i], src);
printPath(parent, i);
}
}
printf("\n");
}
// 实现Dijkstra算法的函数
void dijkstra(int graph[V][V], int src) {
int dist[V]; // 保存源到i的最短距离
bool sptSet[V]; // 如果顶点i在最短路径树中或距离已确定,则为true
int parent[V]; // 保存最短路径树
// 初始化所有距离为INFINITE,sptSet[]为false
for (int i = 0; i < V; i++) {
dist[i] = INT_MAX;
sptSet[i] = false;
parent[src] = -1; // 源节点的父节点设为-1
}
// 源顶点到自身的距离总是0
dist[src] = 0;
// 找到所有顶点的最短路径
for (int count = 0; count < V - 1; count++) {
// 从尚未处理的顶点集中选择最小距离顶点
int u = minDistance(dist, sptSet);
// 标记选定的顶点为已处理
sptSet[u] = true;
// 更新选定顶点的相邻顶点的dist值
for (int v = 0; v < V; v++) {
// 更新dist[v]仅当:
// 1. 不在sptSet中
// 2. 存在从u到v的边
// 3. 通过u到v的路径的总权重小于dist[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];
parent[v] = u;
}
}
}
// 打印构造的距离数组和路径
printSolution(dist, parent, src);
}
int main() {
// 用邻接矩阵表示图
int graph[V][V] = { {0, 4, 0, 0, 0, 0},
{4, 0, 8, 0, 0, 0},
{0, 8, 0, 7, 0, 4},
{0, 0, 7, 0, 9, 14},
{0, 0, 0, 9, 0, 10},
{0, 0, 4, 14, 10, 0} };
printf("Dijkstra算法实现 - 最短路径:\n");
dijkstra(graph, 0); // 从顶点0开始计算最短路径
return 0;
}
二、Prim最小生成树算法
算法原理
Prim算法用于在加权无向连通图中寻找最小生成树(MST)。它从任意顶点开始,逐步扩展树,每次添加与当前树相连且权重最小的边。
核心思想:
-
初始化:选择任意顶点作为起始点
-
维护一个优先队列存储连接树与非树顶点的边
-
每次取出权重最小的边加入生成树
-
更新与新加入顶点相连的边
-
重复直到包含所有顶点
时间复杂度:O(ElogV)使用优先队列
应用场景
-
网络设计(如电缆布线)
-
集群分析
-
图像分割
C语言实现
#include <stdio.h>
#include <limits.h>
#include <stdbool.h>
#define V 5
int minKey(int key[], bool mstSet[]) {
int min = INT_MAX, min_index;
for (int v = 0; v < V; v++)
if (!mstSet[v] && key[v] < min)
min = key[v], min_index = v;
return min_index;
}
void printMST(int parent[], int graph[V][V]) {
printf("边 \t权重\n");
for (int i = 1; i < V; i++)
printf("%d - %d \t%d \n", parent[i], i, graph[i][parent[i]]);
}
void primMST(int graph[V][V]) {
int parent[V]; // 存储构造的MST
int key[V]; // 选取最小权重的边
bool mstSet[V]; // 标记顶点是否已在MST中
for (int i = 0; i < V; i++)
key[i] = INT_MAX, mstSet[i] = false;
key[0] = 0; // 第一个顶点作为起点
parent[0] = -1; // 根节点没有父节点
for (int count = 0; count < V - 1; count++) {
int u = minKey(key, mstSet);
mstSet[u] = true;
for (int v = 0; v < V; v++)
if (graph[u][v] && !mstSet[v] && graph[u][v] < key[v])
parent[v] = u, key[v] = graph[u][v];
}
printMST(parent, graph);
}
int main() {
int graph[V][V] = { {0, 2, 0, 6, 0},
{2, 0, 3, 8, 5},
{0, 3, 0, 0, 7},
{6, 8, 0, 0, 9},
{0, 5, 7, 9, 0} };
printf("Prim算法生成的最小生成树:\n");
primMST(graph);
return 0;
}
三、Kruskal最小生成树算法
算法原理
Kruskal算法同样用于寻找MST,但与Prim算法不同,它按照边的权重顺序依次选择,并确保不形成环。
核心思想:
-
将所有边按权重升序排序
-
初始化一个空集合作为生成树
-
依次选择最小边,如果加入该边不会形成环,则加入生成树
-
使用并查集(Disjoint Set)数据结构高效检测环
时间复杂度:O(ElogE)或O(ElogV)
应用场景
-
电路板布线
-
网络设计
-
近似算法的基础
C语言实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define V 5
#define E 7
struct Edge {
int src, dest, weight;
};
struct Graph {
int V, E;
struct Edge* edge;
};
struct subset {
int parent;
int rank;
};
int find(struct subset subsets[], int i) {
if (subsets[i].parent != i)
subsets[i].parent = find(subsets, subsets[i].parent);
return subsets[i].parent;
}
void Union(struct subset subsets[], int x, int y) {
int xroot = find(subsets, x);
int yroot = find(subsets, y);
if (subsets[xroot].rank < subsets[yroot].rank)
subsets[xroot].parent = yroot;
else if (subsets[xroot].rank > subsets[yroot].rank)
subsets[yroot].parent = xroot;
else {
subsets[yroot].parent = xroot;
subsets[xroot].rank++;
}
}
int myComp(const void* a, const void* b) {
struct Edge* a1 = (struct Edge*)a;
struct Edge* b1 = (struct Edge*)b;
return a1->weight > b1->weight;
}
void KruskalMST(struct Graph* graph) {
int V = graph->V;
struct Edge result[V];
int e = 0;
int i = 0;
qsort(graph->edge, graph->E, sizeof(graph->edge[0]), myComp);
struct subset* subsets = (struct subset*)malloc(V * sizeof(struct subset));
for (int v = 0; v < V; ++v) {
subsets[v].parent = v;
subsets[v].rank = 0;
}
while (e < V - 1 && i < graph->E) {
struct Edge next_edge = graph->edge[i++];
int x = find(subsets, next_edge.src);
int y = find(subsets, next_edge.dest);
if (x != y) {
result[e++] = next_edge;
Union(subsets, x, y);
}
}
printf("Kruskal算法生成的最小生成树:\n");
for (i = 0; i < e; ++i)
printf("%d -- %d == %d\n", result[i].src, result[i].dest, result[i].weight);
}
int main() {
struct Graph* graph = (struct Graph*)malloc(sizeof(struct Graph));
graph->V = V;
graph->E = E;
graph->edge = (struct Edge*)malloc(graph->E * sizeof(struct Edge));
// 示例图
graph->edge[0].src = 0;
graph->edge[0].dest = 1;
graph->edge[0].weight = 2;
graph->edge[1].src = 0;
graph->edge[1].dest = 3;
graph->edge[1].weight = 6;
graph->edge[2].src = 1;
graph->edge[2].dest = 2;
graph->edge[2].weight = 3;
graph->edge[3].src = 1;
graph->edge[3].dest = 3;
graph->edge[3].weight = 8;
graph->edge[4].src = 1;
graph->edge[4].dest = 4;
graph->edge[4].weight = 5;
graph->edge[5].src = 2;
graph->edge[5].dest = 4;
graph->edge[5].weight = 7;
graph->edge[6].src = 3;
graph->edge[6].dest = 4;
graph->edge[6].weight = 9;
KruskalMST(graph);
return 0;
}
四、拓扑排序算法
算法原理
拓扑排序是对有向无环图(DAG)的线性排序,使得对于图中的每条有向边(u,v),u在排序中总位于v之前。
核心思想:
-
计算所有顶点的入度
-
将入度为0的顶点加入队列
-
从队列中取出顶点并输出
-
将该顶点邻接顶点的入度减1,若减至0则加入队列
-
重复直到队列为空
时间复杂度:O(V+E)
应用场景
-
任务调度
-
课程安排
-
编译器的依赖解析
C语言实现
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define V 6
struct Node {
int vertex;
struct Node* next;
};
struct Graph {
struct Node** adjLists;
int* indegree;
};
struct Node* createNode(int v) {
struct Node* newNode = malloc(sizeof(struct Node));
newNode->vertex = v;
newNode->next = NULL;
return newNode;
}
struct Graph* createGraph(int vertices) {
struct Graph* graph = malloc(sizeof(struct Graph));
graph->adjLists = malloc(vertices * sizeof(struct Node*));
graph->indegree = malloc(vertices * sizeof(int));
for (int i = 0; i < vertices; i++) {
graph->adjLists[i] = NULL;
graph->indegree[i] = 0;
}
return graph;
}
void addEdge(struct Graph* graph, int src, int dest) {
// 添加边从src到dest
struct Node* newNode = createNode(dest);
newNode->next = graph->adjLists[src];
graph->adjLists[src] = newNode;
// 更新入度
graph->indegree[dest]++;
}
void topologicalSort(struct Graph* graph) {
int queue[V];
int front = -1, rear = -1;
// 将所有入度为0的顶点加入队列
for (int i = 0; i < V; i++) {
if (graph->indegree[i] == 0) {
queue[++rear] = i;
}
}
int count = 0;
int topOrder[V];
int index = 0;
while (front != rear) {
int u = queue[++front];
topOrder[index++] = u;
struct Node* temp = graph->adjLists[u];
while (temp != NULL) {
int v = temp->vertex;
if (--graph->indegree[v] == 0) {
queue[++rear] = v;
}
temp = temp->next;
}
count++;
}
if (count != V) {
printf("图中存在环,无法进行拓扑排序\n");
return;
}
printf("拓扑排序结果: ");
for (int i = 0; i < V; i++)
printf("%d ", topOrder[i]);
printf("\n");
}
int main() {
struct Graph* graph = createGraph(V);
addEdge(graph, 5, 2);
addEdge(graph, 5, 0);
addEdge(graph, 4, 0);
addEdge(graph, 4, 1);
addEdge(graph, 2, 3);
addEdge(graph, 3, 1);
topologicalSort(graph);
return 0;
}
总结比较
| 算法 | 适用图类型 | 解决的问题 | 时间复杂度 | 特点 |
|---|---|---|---|---|
| Dijkstra | 带权有向/无向图(非负权) | 单源最短路径 | O((V+E)logV) | 贪心算法,使用优先队列 |
| Prim | 带权无向连通图 | 最小生成树 | O(ElogV) | 从顶点扩展,适合稠密图 |
| Kruskal | 带权无向连通图 | 最小生成树 | O(ElogE) | 按边选择,适合稀疏图 |
| 拓扑排序 | 有向无环图(DAG) | 任务排序 | O(V+E) | 基于入度,检测环 |
每种算法都有其特定的应用场景和优势,理解它们的原理和实现对于解决实际问题至关重要。在实际应用中,应根据具体问题的特点选择合适的算法。
Floyd-Warshall算法:全源最短路径的经典解法
算法原理
Floyd-Warshall算法是解决全源最短路径问题的动态规划算法,可以处理包含负权边(但不含负权回路)的图。该算法由Robert Floyd和Stephen Warshall在1962年分别独立提出。
核心思想:
-
使用动态规划逐步改善最短路径估计
-
定义dᵏᵢⱼ为从i到j且中间顶点编号不超过k的最短路径长度
-
通过三重循环逐步放松所有可能的路径
状态转移方程:
dᵏᵢⱼ = min(dᵏ⁻¹ᵢⱼ, dᵏ⁻¹ᵢₖ + dᵏ⁻¹ₖⱼ)
时间复杂度:O(V³) —— 非常适合稠密图
空间复杂度:O(V²)
算法特点
-
优点:
-
代码实现简单
-
可以直接处理负权边(只要没有负权回路)
-
一次计算即可获得所有顶点对的最短路径
-
-
缺点:
-
时间复杂度较高,不适合大规模稀疏图
-
不能处理包含负权回路的图
-
应用场景
-
网络路由优化
-
交通网络分析
-
社交网络中的关系距离计算
-
任意两点间最短路径的预计算
C语言实现
#include <stdio.h>
#define V 4
#define INF 99999
void printSolution(int dist[][V]);
void floydWarshall(int graph[][V]) {
int dist[V][V], i, j, k;
// 初始化距离矩阵
for (i = 0; i < V; i++)
for (j = 0; j < V; j++)
dist[i][j] = graph[i][j];
// 逐步放松所有顶点作为中间点
for (k = 0; k < V; k++) {
for (i = 0; i < V; i++) {
for (j = 0; j < V; j++) {
// 如果通过k顶点路径更短,则更新
if (dist[i][k] + dist[k][j] < dist[i][j])
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
// 打印最终的最短距离矩阵
printSolution(dist);
}
void printSolution(int dist[][V]) {
printf("Floyd-Warshall算法计算的全源最短路径矩阵:\n");
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
if (dist[i][j] == INF)
printf("%7s", "INF");
else
printf("%7d", dist[i][j]);
}
printf("\n");
}
}
int main() {
/* 示例图
10
(0)------->(3)
| /|\
5 | |
| | 1
\|/ |
(1)------->(2)
3 */
int graph[V][V] = { {0, 5, INF, 10},
{INF, 0, 3, INF},
{INF, INF, 0, 1},
{INF, INF, INF, 0} };
floydWarshall(graph);
return 0;
}
算法执行过程解析
-
初始化阶段:
-
创建距离矩阵dist[][],初始值与图的邻接矩阵相同
-
对角线元素设为0(顶点到自身的距离)
-
不直接相连的顶点距离设为INF
-
-
动态规划阶段:
-
外层循环(k):依次考虑每个顶点作为中间点
-
中层循环(i):遍历所有起点
-
内层循环(j):遍历所有终点
-
检查是否通过顶点k可以缩短i到j的路径
-
-
结果输出:
-
最终dist[i][j]包含从i到j的最短路径距离
-
可以额外维护一个路径矩阵来记录具体路径
-
路径重建的实现
如果需要记录具体路径而不仅仅是距离,可以扩展算法如下:
void floydWarshallWithPath(int graph[][V]) {
int dist[V][V], next[V][V], i, j, k;
// 初始化
for (i = 0; i < V; i++) {
for (j = 0; j < V; j++) {
dist[i][j] = graph[i][j];
if (graph[i][j] != INF && i != j)
next[i][j] = j;
else
next[i][j] = -1;
}
}
// 算法主循环
for (k = 0; k < V; k++) {
for (i = 0; i < V; i++) {
for (j = 0; j < V; j++) {
if (dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
next[i][j] = next[i][k];
}
}
}
}
// 打印结果和路径
printf("最短路径距离矩阵:\n");
printSolution(dist);
printf("\n路径重建示例:\n");
for (i = 0; i < V; i++) {
for (j = 0; j < V; j++) {
if (i != j && next[i][j] != -1) {
printf("从 %d 到 %d 的路径: %d", i, j, i);
int u = i, v = j;
while (u != v) {
u = next[u][v];
printf(" -> %d", u);
}
printf(",总距离: %d\n", dist[i][j]);
}
}
}
}
算法优化与变种
-
空间优化:可以使用单个距离矩阵,原地更新
-
提前终止:检测到负权回路时可以提前终止
-
并行化:由于三重循环的独立性,适合并行计算
-
稀疏图优化:对于稀疏图可以结合Dijkstra算法改进
与其他算法的比较
| 特性 | Floyd-Warshall | Dijkstra(所有顶点) | Johnson算法 |
|---|---|---|---|
| 时间复杂度 | O(V³) | O(V(E+VlogV)) | O(VE+V²logV) |
| 负权边 | 支持 | 不支持 | 支持 |
| 负权回路 | 可检测 | 不可检测 | 可检测 |
| 实现复杂度 | 简单 | 中等 | 较复杂 |
| 适用场景 | 稠密图 | 稀疏图 | 稀疏图+负权 |
实际应用案例
-
交通网络分析:计算城市所有地点之间的最短行车路线
-
网络路由协议:某些路由协议中预计算所有节点间的最短路径
-
社交网络:计算社交图中任意两人之间的"关系距离"
-
游戏开发:在战略游戏中预计算地图上所有位置间的移动成本
完整测试用例
#include <stdio.h>
#define V 5
#define INF 99999
void printSolution(int dist[][V]);
void floydWarshall(int graph[][V]) {
/* 实现内容同上 */
}
int main() {
// 测试用例1:常规图
int graph1[V][V] = { {0, 3, 8, INF, -4},
{INF, 0, INF, 1, 7},
{INF, 4, 0, INF, INF},
{2, INF, -5, 0, INF},
{INF, INF, INF, 6, 0} };
printf("测试用例1:\n");
floydWarshall(graph1);
// 测试用例2:包含负权边但不含负权回路
int graph2[V][V] = { {0, INF, -2, INF, INF},
{4, 0, 3, INF, INF},
{INF, INF, 0, 2, INF},
{INF, -1, INF, 0, INF},
{INF, INF, INF, 1, 0} };
printf("\n测试用例2:\n");
floydWarshall(graph2);
// 测试用例3:完全连通图
int graph3[V][V] = { {0, 2, 2, 2, 2},
{2, 0, 2, 2, 2},
{2, 2, 0, 2, 2},
{2, 2, 2, 0, 2},
{2, 2, 2, 2, 0} };
printf("\n测试用例3:\n");
floydWarshall(graph3);
return 0;
}
Floyd-Warshall算法以其简洁的实现和强大的功能,成为图算法中不可或缺的工具。虽然时间复杂度较高,但对于中等规模的图或需要全源最短路径的场景,它仍然是首选算法。理解并掌握这一算法,对于解决复杂的网络优化问题具有重要意义。
2359

被折叠的 条评论
为什么被折叠?



