本文献给:
已掌握无向图基础,希望理解如何在带权图中找到两点间最短路径的C语言学习者。本文将系统讲解两种经典的最短路径算法。
你将学到:
- 最短路径问题的定义与核心概念
- Dijkstra算法:解决单源、非负权图的最短路径
- Bellman-Ford算法:处理单源、含负权边的最短路径
- 算法对比与实战应用
目录
- 第一部分:问题定义与核心概念
-
- [1. 什么是最短路径?](#1. 什么是最短路径?)
- 第二部分:图的存储(带权图)
- 第三部分:Dijkstra算法
-
- [1. 核心思想](#1. 核心思想)
- [2. 算法步骤(朴素 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2) 实现)</b>](#2. 算法步骤(朴素 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2) 实现))
- [3. C语言实现](#3. C语言实现)
- 第四部分:Bellman-Ford算法
-
- [1. 核心思想](#1. 核心思想)
- [2. 算法步骤](#2. 算法步骤)
- [3. C语言实现](#3. C语言实现)
- 第五部分:总结与对比
第一部分:问题定义与核心概念
1. 什么是最短路径?
在带权无向图 G = ( V , E , w ) G = (V, E, w) G=(V,E,w) 中, w : E → R w: E \rightarrow \mathbb{R} w:E→R 为边权函数。从源点 s s s 到目标 t t t 的路径 P = ( s , v 1 , . . . , v k , t ) P = (s, v_1, ..., v_k, t) P=(s,v1,...,vk,t) 的长度为路径上所有边权之和:
d i s t ( P ) = ∑ i = 0 k w ( v i , v i + 1 ) dist(P) = \sum_{i=0}^{k} w(v_i, v_{i+1}) dist(P)=i=0∑kw(vi,vi+1)
最短路径 是所有 s s s 到 t t t 路径中长度最小的路径。
关键术语:
- 单源最短路径 :求从一个源点 s s s 到图中所有其他顶点的最短距离。
- 权值:可正、可负(实际问题中常为非负,如距离、时间、成本)。
- 松弛操作:最短路径算法的核心操作,通过考察一条边来更新当前的最短距离估计。
第二部分:图的存储(带权图)
在基础邻接矩阵/邻接表上,需要增加权值信息。
c
#define MAX_V 100
#define INF 0x3f3f3f3f // 表示"无穷大"的一个较大数值
// 邻接矩阵(带权)
typedef struct {
int matrix[MAX_V][MAX_V]; // 存储权值,INF表示无边
int vertex_count;
} GraphMatrixWeighted;
void init_graph_weighted(GraphMatrixWeighted* g, int n) {
g->vertex_count = n;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
g->matrix[i][j] = (i == j) ? 0 : INF; // 自身距离为0
}
}
}
void add_edge_weighted(GraphMatrixWeighted* g, int u, int v, int weight) {
if (u >= g->vertex_count || v >= g->vertex_count) return;
g->matrix[u][v] = weight;
g->matrix[v][u] = weight; // 无向图
}
第三部分:Dijkstra算法
1. 核心思想
贪心策略 。维护一个集合 S S S,包含已确定最短距离的顶点。每次从未确定的顶点中选择距离源点 s s s 最近的顶点 u u u 加入 S S S,并用 u u u 来松弛 其所有邻居的距离。要求:所有边权非负。
算法正确性依赖:当边权非负时,当前未确定顶点中距离最小的顶点,其距离不可能再被其他未确定的顶点更新减小。
2. 算法步骤(朴素 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2) 实现)
- 初始化:
dist[s] = 0,dist[v] = INF(v ≠ s),visited[v] = false。 - 循环
|V|次:
a. 找到未访问顶点中dist最小的顶点u。
b. 标记u为已访问 (visited[u] = true)。
c. 松弛操作 :对u的每个邻居v,如果dist[u] + w(u, v) < dist[v],则更新dist[v] = dist[u] + w(u, v)。 - 循环结束,
dist[]数组即为源点到各点的最短距离。
3. C语言实现
c
// Dijkstra算法 (邻接矩阵,朴素实现)
void dijkstra(GraphMatrixWeighted* g, int src, int dist[]) {
int visited[MAX_V] = {0};
// 初始化
for (int i = 0; i < g->vertex_count; i++) {
dist[i] = INF;
}
dist[src] = 0;
for (int count = 0; count < g->vertex_count; count++) {
// 步骤1: 找到未访问的dist最小顶点
int u = -1;
int min_dist = INF;
for (int v = 0; v < g->vertex_count; v++) {
if (!visited[v] && dist[v] < min_dist) {
min_dist = dist[v];
u = v;
}
}
if (u == -1) break; // 剩余顶点不可达
visited[u] = 1;
// 步骤2: 松弛u的所有邻居
for (int v = 0; v < g->vertex_count; v++) {
if (!visited[v] && g->matrix[u][v] != INF) {
int new_dist = dist[u] + g->matrix[u][v];
if (new_dist < dist[v]) {
dist[v] = new_dist;
}
}
}
}
}
算法复杂度:
- 时间复杂度 : O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2),适合稠密图。
- 空间复杂度 : O ( ∣ V ∣ ) O(|V|) O(∣V∣)。
- 优化方向 :使用**优先队列(最小堆)**可将时间复杂度降为 O ( ( ∣ V ∣ + ∣ E ∣ ) log ∣ V ∣ ) O((|V|+|E|) \log |V|) O((∣V∣+∣E∣)log∣V∣),适合稀疏图。
第四部分:Bellman-Ford算法
1. 核心思想
动态规划/松弛 。通过对所有边进行 ∣ V ∣ − 1 |V|-1 ∣V∣−1 轮松弛操作,逐步逼近最短路径。可以处理负权边 ,并能检测出图中是否存在从源点可达的负权环。
原理 :在无负权环的图中,任意两点间的最短路径最多包含 ∣ V ∣ − 1 |V|-1 ∣V∣−1 条边。因此通过 ∣ V ∣ − 1 |V|-1 ∣V∣−1 轮全局松弛足以找到所有最短路径。
2. 算法步骤
- 初始化:
dist[s] = 0,dist[v] = INF(v ≠ s)。 - 进行 ∣ V ∣ − 1 |V|-1 ∣V∣−1 轮迭代,每轮:
- 遍历图中的所有边 ( u , v ) (u, v) (u,v)。
- 对每条边执行松弛操作 :如果
dist[u] + w(u, v) < dist[v],则更新dist[v] = dist[u] + w(u, v)。
- 负权环检测:再进行一轮所有边的遍历。如果仍有边能被松弛,则说明图中存在从源点可达的负权环,最短路径无意义。
3. C语言实现
c
// Bellman-Ford算法 (使用边列表存储图更合适,此处为演示使用邻接矩阵遍历所有边)
void bellman_ford(GraphMatrixWeighted* g, int src, int dist[]) {
// 初始化
for (int i = 0; i < g->vertex_count; i++) {
dist[i] = INF;
}
dist[src] = 0;
// 步骤1: 进行 |V|-1 轮松弛
for (int i = 0; i < g->vertex_count - 1; i++) {
// 遍历所有边 (u, v)
for (int u = 0; u < g->vertex_count; u++) {
for (int v = 0; v < g->vertex_count; v++) {
if (g->matrix[u][v] != INF) { // 存在边
int new_dist = dist[u] + g->matrix[u][v];
if (new_dist < dist[v]) {
dist[v] = new_dist;
}
}
}
}
}
// 步骤2: 检测负权环 (可选,如果需要检测)
for (int u = 0; u < g->vertex_count; u++) {
for (int v = 0; v < g->vertex_count; v++) {
if (g->matrix[u][v] != INF && dist[u] + g->matrix[u][v] < dist[v]) {
printf("图中存在从源点可达的负权环!\n");
return;
}
}
}
}
算法复杂度:
- 时间复杂度 : O ( ∣ V ∣ ⋅ ∣ E ∣ ) O(|V| \cdot |E|) O(∣V∣⋅∣E∣),使用邻接矩阵为 O ( ∣ V ∣ 3 ) O(|V|^3) O(∣V∣3),使用边列表为 O ( ∣ V ∣ ⋅ ∣ E ∣ ) O(|V| \cdot |E|) O(∣V∣⋅∣E∣)。
- 空间复杂度 : O ( ∣ V ∣ ) O(|V|) O(∣V∣)。
第五部分:总结与对比
算法对比表
| 特性 | Dijkstra算法 | Bellman-Ford算法 |
|---|---|---|
| 适用图类型 | 非负权图 | 任意图(可含负权边) |
| 核心思想 | 贪心 | 动态规划/松弛 |
| 时间复杂度 | O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2) 或 O ( ( ∣ V ∣ + ∣ E ∣ ) log ∣ V ∣ ) O((|V|+|E|)\log|V|) O((∣V∣+∣E∣)log∣V∣) | O ( ∣ V ∣ ⋅ ∣ E ∣ ) O(|V| \cdot |E|) O(∣V∣⋅∣E∣) |
| 空间复杂度 | O ( ∣ V ∣ ) O(|V|) O(∣V∣) | O ( ∣ V ∣ ) O(|V|) O(∣V∣) |
| 功能 | 求单源最短路径 | 求单源最短路径,可检测负权环 |
| 优点 | 在非负权图中效率高 | 功能强大,适用范围广 |
| 缺点 | 不能处理负权边 | 时间复杂度高 |
选择指南
- 绝大多数情况(边权非负) :优先使用 Dijkstra算法(尤其是堆优化版本)。例如:道路导航、网络路由。
- 图含有负权边 :必须使用 Bellman-Ford算法。例如:金融套利模型、某些特殊约束问题。
- 需要检测负权环:使用 Bellman-Ford 算法。
觉得文章有帮助?别忘了:
👍 点赞 👍 - 给我一点鼓励
⭐ 收藏 ⭐ - 方便以后查看
🔔 关注 🔔 - 获取更新通知
标签: #C语言 #图论 #最短路径 #Dijkstra #Bellman-Ford #算法