当前位置: 首页 > ds >正文

【考研408数据结构-09】 图论进阶:最短路径与最小生成树

📚 【考研408数据结构-09】 图论进阶:最短路径与最小生成树

🎯 考频:⭐⭐⭐⭐⭐ | 题型:选择题、综合应用题、算法设计题 | 分值:约10-18分

引言

想象你是一家物流公司的路线规划师。面对全国数百个配送站点,如何找到从总仓到各个站点的最短配送路径?如何用最少的成本连接所有站点构建配送网络?这些实际问题的本质就是图论中的最短路径和最小生成树问题。

在408考试中,最短路径和最小生成树算法是图论部分的核心考点,几乎每年必考。据统计,近5年的408真题中,Dijkstra算法出现7次,Prim/Kruskal算法出现6次,Floyd算法和关键路径也频繁出现在综合应用题中。这部分内容不仅分值高,而且是算法设计题的热门选择。

本文将深入剖析四大最短路径算法(Dijkstra、Floyd)和两大最小生成树算法(Prim、Kruskal),并掌握关键路径的求解方法。

学完本文,你将能够:

  1. ✅ 熟练掌握各算法的执行过程和实现细节
  2. ✅ 准确分析算法复杂度并选择合适算法
  3. ✅ 手工模拟算法执行步骤(408必考)
  4. ✅ 解决实际的网络优化问题

一、知识精讲

1.1 概念定义

最短路径问题

单源最短路径(Single-Source Shortest Path):求一个顶点到其他所有顶点的最短路径

  • Dijkstra算法:适用于非负权图,贪心策略
  • Bellman-Ford算法:可处理负权边(408了解即可)

多源最短路径(All-Pairs Shortest Path):求任意两顶点间的最短路径

  • Floyd-Warshall算法:动态规划思想,简洁高效

最小生成树(Minimum Spanning Tree, MST):连通所有顶点且边权和最小的树

  • Prim算法:从顶点出发,逐步扩展
  • Kruskal算法:从边出发,逐步连接

关键路径(Critical Path):AOE网中的最长路径,决定项目完成时间

⚠️ 408考纲要求

  • 掌握:Dijkstra、Prim、Kruskal算法
  • 理解:Floyd算法、关键路径
  • 了解:Bellman-Ford、SPFA算法

1.2 原理分析

1.2.1 Dijkstra算法原理

核心思想:贪心策略,每次选择当前最短的路径确定下来

  1. 初始化:源点距离为0,其他为∞
  2. 选择未确定的最短距离顶点u
  3. 更新u的邻接点距离(松弛操作)
  4. 重复直到所有顶点确定

关键点

  • 不能处理负权边(贪心选择的局限)
  • 使用优先队列可优化到O(ElogV)
1.2.2 Floyd算法原理

核心思想:动态规划,考虑是否经过中间顶点k

  • 状态定义:dist[i][j]表示i到j的最短距离
  • 状态转移:dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
  • 三重循环:k为中间顶点,i、j为起终点
1.2.3 Prim算法原理

核心思想:从一个顶点开始,逐步扩展生成树

  1. 任选一个顶点加入MST
  2. 选择连接MST和非MST顶点的最小边
  3. 将该边和顶点加入MST
  4. 重复直到包含所有顶点
1.2.4 Kruskal算法原理

核心思想:按边权排序,逐个加入不构成环的边

  1. 将所有边按权值排序
  2. 依次选择最小边
  3. 若不构成环则加入MST(并查集判断)
  4. 直到选够n-1条边

1.3 性质与特点

算法时间复杂度空间复杂度适用场景408考频
DijkstraO(V²)或O(ElogV)O(V)单源非负权⭐⭐⭐⭐⭐
FloydO(V³)O(V²)多源任意权⭐⭐⭐⭐
PrimO(V²)或O(ElogV)O(V)稠密图MST⭐⭐⭐⭐
KruskalO(ElogE)O(V)稀疏图MST⭐⭐⭐⭐⭐

算法选择策略

  • 单源最短路径:优先Dijkstra(非负权)
  • 多源最短路径:Floyd(顶点少)
  • 稠密图MST:Prim算法
  • 稀疏图MST:Kruskal算法

二、代码实现

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <stdbool.h>#define MAXV 100
#define INF INT_MAX// ========== 图的邻接矩阵表示 ==========
typedef struct {int vexnum, arcnum;int arcs[MAXV][MAXV];  // 边权矩阵char vexs[MAXV];
} MGraph;// ========== Dijkstra算法实现 ==========
void Dijkstra(MGraph *G, int src, int dist[], int path[]) {bool visited[MAXV];// 初始化for (int i = 0; i < G->vexnum; i++) {dist[i] = G->arcs[src][i];  // 初始距离visited[i] = false;          // 未访问path[i] = (dist[i] < INF) ? src : -1;  // 前驱}dist[src] = 0;visited[src] = true;// 主循环:找n-1个顶点的最短路径for (int i = 1; i < G->vexnum; i++) {int min = INF, u = -1;// 找未访问的最小距离顶点for (int j = 0; j < G->vexnum; j++) {if (!visited[j] && dist[j] < min) {min = dist[j];u = j;}}if (u == -1) break;  // 无可达顶点visited[u] = true;// 松弛操作:更新u的邻接点距离for (int v = 0; v < G->vexnum; v++) {if (!visited[v] && G->arcs[u][v] < INF) {if (dist[u] + G->arcs[u][v] < dist[v]) {dist[v] = dist[u] + G->arcs[u][v];path[v] = u;  // 更新前驱}}}}
}// 打印最短路径
void PrintPath(int path[], int v) {if (path[v] == -1) {printf("无路径");return;}int stack[MAXV], top = -1;while (v != -1) {stack[++top] = v;v = path[v];if (top > 0 && v == stack[top-1]) break;  // 避免死循环}while (top >= 0) {printf("%d ", stack[top--]);if (top >= 0) printf("-> ");}
}// ========== Floyd算法实现 ==========
void Floyd(MGraph *G, int dist[][MAXV], int path[][MAXV]) {// 初始化for (int i = 0; i < G->vexnum; i++) {for (int j = 0; j < G->vexnum; j++) {dist[i][j] = G->arcs[i][j];path[i][j] = (G->arcs[i][j] < INF && i != j) ? i : -1;}}// 三重循环:k为中间顶点for (int k = 0; k < G->vexnum; k++) {for (int i = 0; i < G->vexnum; i++) {for (int j = 0; j < G->vexnum; j++) {// 防止溢出if (dist[i][k] != INF && dist[k][j] != INF) {if (dist[i][k] + dist[k][j] < dist[i][j]) {dist[i][j] = dist[i][k] + dist[k][j];path[i][j] = path[k][j];  // 更新前驱}}}}}
}// ========== Prim算法实现 ==========
int Prim(MGraph *G, int start) {int lowcost[MAXV];  // 到MST的最小代价int closest[MAXV];  // 最近的MST顶点bool inMST[MAXV];   // 是否在MST中int sum = 0;        // MST权值和// 初始化for (int i = 0; i < G->vexnum; i++) {lowcost[i] = G->arcs[start][i];closest[i] = start;inMST[i] = false;}inMST[start] = true;// 选择n-1条边for (int i = 1; i < G->vexnum; i++) {int min = INF, k = -1;// 找最小边for (int j = 0; j < G->vexnum; j++) {if (!inMST[j] && lowcost[j] < min) {min = lowcost[j];k = j;}}if (k == -1) return -1;  // 图不连通printf("边(%d,%d) 权值:%d\n", closest[k], k, min);sum += min;inMST[k] = true;// 更新lowcostfor (int j = 0; j < G->vexnum; j++) {if (!inMST[j] && G->arcs[k][j] < lowcost[j]) {lowcost[j] = G->arcs[k][j];closest[j] = k;}}}return sum;
}// ========== Kruskal算法(使用并查集)==========
typedef struct {int u, v, w;  // 边的两个顶点和权值
} Edge;// 并查集查找
int Find(int parent[], int x) {if (parent[x] != x) {parent[x] = Find(parent, parent[x]);  // 路径压缩}return parent[x];
}// 并查集合并
void Union(int parent[], int rank[], int x, int y) {int rootX = Find(parent, x);int rootY = Find(parent, y);if (rootX != rootY) {if (rank[rootX] < rank[rootY]) {parent[rootX] = rootY;} else if (rank[rootX] > rank[rootY]) {parent[rootY] = rootX;} else {parent[rootY] = rootX;rank[rootX]++;}}
}// 边的比较函数(用于排序)
int compareEdge(const void *a, const void *b) {return ((Edge*)a)->w - ((Edge*)b)->w;
}int Kruskal(MGraph *G, Edge edges[], int edgeNum) {int parent[MAXV], rank[MAXV];int sum = 0, count = 0;// 初始化并查集for (int i = 0; i < G->vexnum; i++) {parent[i] = i;rank[i] = 0;}// 边按权值排序qsort(edges, edgeNum, sizeof(Edge), compareEdge);// 依次选择边for (int i = 0; i < edgeNum && count < G->vexnum - 1; i++) {int u = edges[i].u;int v = edges[i].v;// 判断是否构成环if (Find(parent, u) != Find(parent, v)) {printf("选择边(%d,%d) 权值:%d\n", u, v, edges[i].w);Union(parent, rank, u, v);sum += edges[i].w;count++;}}return (count == G->vexnum - 1) ? sum : -1;  // 判断是否连通
}// ========== 关键路径算法 ==========
typedef struct {int ve[MAXV];  // 事件最早发生时间int vl[MAXV];  // 事件最晚发生时间
} CriticalPath;bool TopologicalSort_CP(MGraph *G, int ve[], int stack[]) {int indegree[MAXV] = {0};int queue[MAXV], front = 0, rear = 0;int count = 0, top = -1;// 计算入度for (int i = 0; i < G->vexnum; i++) {for (int j = 0; j < G->vexnum; j++) {if (G->arcs[i][j] < INF && G->arcs[i][j] > 0) {indegree[j]++;}}}// 初始化vefor (int i = 0; i < G->vexnum; i++) {ve[i] = 0;if (indegree[i] == 0) {queue[rear++] = i;}}// 拓扑排序并计算vewhile (front != rear) {int u = queue[front++];stack[++top] = u;  // 记录拓扑序列count++;for (int v = 0; v < G->vexnum; v++) {if (G->arcs[u][v] < INF && G->arcs[u][v] > 0) {indegree[v]--;if (ve[u] + G->arcs[u][v] > ve[v]) {ve[v] = ve[u] + G->arcs[u][v];}if (indegree[v] == 0) {queue[rear++] = v;}}}}return count == G->vexnum;
}void CriticalPathMethod(MGraph *G) {int ve[MAXV], vl[MAXV];int stack[MAXV], top = -1;// 拓扑排序并计算veif (!TopologicalSort_CP(G, ve, stack)) {printf("图中存在环,无法计算关键路径\n");return;}top = G->vexnum - 1;// 初始化vl为ve[n-1]for (int i = 0; i < G->vexnum; i++) {vl[i] = ve[G->vexnum - 1];}// 逆拓扑序计算vlwhile (top >= 0) {int u = stack[top--];for (int v = 0; v < G->vexnum; v++) {if (G->arcs[u][v] < INF && G->arcs[u][v] > 0) {if (vl[v] - G->arcs[u][v] < vl[u]) {vl[u] = vl[v] - G->arcs[u][v];}}}}// 输出关键活动printf("关键活动:\n");for (int u = 0; u < G->vexnum; u++) {for (int v = 0; v < G->vexnum; v++) {if (G->arcs[u][v] < INF && G->arcs[u][v] > 0) {int e = ve[u];  // 活动最早开始时间int l = vl[v] - G->arcs[u][v];  // 活动最晚开始时间if (e == l) {printf("<%d,%d> ", u, v);}}}}
}

复杂度分析

  • Dijkstra:O(V²),堆优化后O(ElogV)
  • Floyd:O(V³),空间O(V²)
  • Prim:O(V²),堆优化后O(ElogV)
  • Kruskal:O(ElogE),主要是排序时间

三、图解说明

【图1】Dijkstra算法执行过程

初始图(源点0):1 --3-- 3/|       |\2 |       | 1/  |       |  \
0   2       4   5\  |       |  /1 |       | 2\|       |/2 --3-- 4步骤1: dist=[0,2,1,∞,∞,∞] 选择顶点2
步骤2: dist=[0,2,1,5,3,∞] 选择顶点1  
步骤3: dist=[0,2,1,5,3,∞] 选择顶点4
步骤4: dist=[0,2,1,5,3,6] 选择顶点3
步骤5: dist=[0,2,1,4,3,5] 选择顶点5最短路径树:0/ \2   4|   |1   3--5

【图2】Prim算法构建MST

步骤1: 选择顶点0        步骤2: 选边(0,2)权1      步骤3: 选边(2,1)权20                       0                         0/                         /2                          2---1步骤4: 选边(0,4)权1     步骤5: 选边(1,3)权30                       0/ \                     / \2   4                   2   4|                       |1                       1---3MST总权值: 1+1+2+3=7

【图3】Floyd算法状态转移

初始矩阵D⁰:          经过顶点0后D¹:        经过顶点0,1后D²:
[0  5  ∞]           [0  5  ∞]            [0  5  8]
[5  0  3]           [5  0  3]            [5  0  3]
[∞  3  0]           [∞  3  0]            [8  3  0]状态转移: D[i][j] = min(D[i][j], D[i][k]+D[k][j])

【图4】关键路径示例

AOE网:v1 --a3(6)-- v3/  \          / \
a1(3)  a2(2)  a5(1) a7(2)/      \    /      \
v0      v2--a4(3)    v5\               /---a6(4)-- v4--a8(1)ve: [0,3,2,6,4,7]  (最早发生时间)
vl: [0,4,3,6,4,7]  (最晚发生时间)关键活动: a1,a5,a7 (e=l的活动)
关键路径: v0->v1->v3->v5 (长度=8)

四、真题演练

📝 真题1(2023年408第42题)

题目:给定带权有向图G,使用Dijkstra算法求顶点0到其他顶点的最短路径。当有多条最短路径时,要求输出经过顶点数最少的那条。请修改算法实现此要求。

解题思路

  1. 在原Dijkstra基础上增加count[]数组记录路径顶点数
  2. 当发现相同最短距离时,选择顶点数更少的路径
  3. 松弛操作时同时更新count数组

参考答案

if (dist[u] + G->arcs[u][v] < dist[v]) {dist[v] = dist[u] + G->arcs[u][v];path[v] = u;count[v] = count[u] + 1;
} else if (dist[u] + G->arcs[u][v] == dist[v]) {if (count[u] + 1 < count[v]) {path[v] = u;count[v] = count[u] + 1;}
}

易错点

  • ⚠️ 忘记初始化count数组
  • ⚠️ 相等情况的判断条件写错

📝 真题2(2022年408算法设计题)

题目:设计算法判断无向图G是否存在一棵包含所有顶点的生成树,如果存在,求出权值最小的生成树的权值和。

解题思路

  1. 先判断图的连通性(DFS或并查集)
  2. 若连通,使用Kruskal或Prim求MST
  3. 返回MST的权值和

评分要点

  • 连通性判断(3分)
  • MST算法实现(5分)
  • 复杂度分析(2分)

📝 真题3(2021年408第23题)

题目:已知AOE网的关键路径长度为18,下列哪个活动一定是关键活动?
A. 最早开始时间为0的活动
B. 最晚开始时间为18的活动
C. 最早开始时间等于最晚开始时间的活动
D. 持续时间最长的活动

答案:C

解析:关键活动的充要条件是e(i)=l(i),即最早开始时间等于最晚开始时间。A、B、D都不是充分条件。

五、在线练习推荐

LeetCode精选题目

  • 743. 网络延迟时间(Medium,Dijkstra)
  • 787. K站中转内最便宜的航班(Medium,最短路径变形)
  • 1135. 最低成本连通所有城市(Medium,MST)
  • 1584. 连接所有点的最小费用(Medium,Kruskal/Prim)

牛客网408专项

  • 最短路径算法专项训练
  • 最小生成树真题集

ACM经典题目

  • POJ 2387 - Til the Cows Come Home(Dijkstra模板)
  • POJ 1287 - Networking(MST模板)

练习顺序建议

  1. 先练习单源最短路径(Dijkstra)
  2. 掌握MST基础题(Prim/Kruskal)
  3. 进阶到Floyd多源最短路径
  4. 最后练习关键路径综合题

六、思维导图

图论进阶算法
├── 最短路径
│   ├── 单源最短路径
│   │   ├── Dijkstra算法
│   │   │   ├── 贪心策略
│   │   │   ├── 非负权图
│   │   │   └── O(V²)/O(ElogV)
│   │   └── Bellman-Ford
│   │       └── 可处理负权
│   └── 多源最短路径
│       └── Floyd算法
│           ├── 动态规划
│           ├── 三重循环
│           └── O(V³)
├── 最小生成树
│   ├── Prim算法
│   │   ├── 从顶点扩展
│   │   ├── 贪心选择
│   │   └── 适合稠密图
│   └── Kruskal算法
│       ├── 从边选择
│       ├── 并查集判环
│       └── 适合稀疏图
└── 关键路径├── AOE网├── ve/vl计算├── 关键活动└── 项目管理应用

七、复习清单

✅ 本章必背知识点清单

概念理解

  • 能准确区分单源和多源最短路径问题
  • 理解最小生成树的定义(n个顶点n-1条边)
  • 掌握关键路径、关键活动的概念
  • 记住各算法的适用条件(负权、稠密/稀疏)

代码实现

  • 能手写Dijkstra算法的完整代码(重点)
  • 能手写Prim或Kruskal算法(至少一个)
  • 掌握Floyd算法的三重循环结构
  • 理解并查集在Kruskal中的应用
  • 记住各算法的时间复杂度

应用能力

  • 会手工模拟Dijkstra算法执行过程
  • 能画出Prim/Kruskal构建MST的步骤
  • 会计算AOE网的ve、vl值
  • 能根据图的特点选择合适算法
  • 掌握算法的优化技巧(堆优化等)

真题要点

  • 理解最短路径的松弛操作
  • 掌握MST的唯一性判断
  • 记住关键活动的判断条件(e=l)
  • 熟悉算法比较和选择的题型
  • 掌握算法设计题的标准模板

八、知识拓展

🔍 常见误区

  1. 误区:Dijkstra算法可以处理所有图
    正解:不能处理负权边,会得到错误结果

  2. 误区:MST一定唯一
    正解:边权可能相同,MST可能不唯一,但权值和唯一

  3. 误区:关键路径就是最短路径
    正解:关键路径是最长路径,决定项目完成时间

  4. 误区:Floyd算法效率低不实用
    正解:顶点少时Floyd最简洁,且能检测负环

  5. 误区:Prim一定比Kruskal好
    正解:稠密图用Prim,稀疏图用Kruskal

💡 记忆技巧

  • Dijkstra:D-贪心(Greedy),每次选"最近"的
  • Floyd:F-浮动(Float),通过中间点"浮动"
  • Prim:P-点(Point),从点开始生长
  • Kruskal:K-边(Edge=Kant),从边开始连接
  • 松弛操作:想象拉紧的绳子被"松弛"到更短路径

📊 自测题

  1. 对于有n个顶点的连通图,其最小生成树有多少条边?
    A. n B. n-1 C. n+1 D. n(n-1)/2

  2. 以下哪个算法不能用于求解最短路径?
    A. Dijkstra B. Floyd C. Prim D. Bellman-Ford

  3. 已知某AOE网的关键路径长度为20,某活动的最早开始时间为5,持续时间为8,则该活动的最晚开始时间可能是?
    A. 5 B. 7 C. 12 D. 13

答案:1.B 2.C 3.B(如果是关键活动则为A)

结语

图论进阶算法是408考试的重头戏,掌握这部分内容对拿高分至关重要。本章我们深入学习了:

🎯 核心要点总结

  1. Dijkstra算法:单源最短路径的首选,记住贪心选择和松弛操作
  2. Floyd算法:多源最短路径的利器,三重循环要理解透彻
  3. Prim vs Kruskal:都是MST算法,关键在于根据图特点选择
  4. 关键路径:项目管理的数学模型,ve/vl计算是核心
  5. 复杂度分析:算法选择的重要依据,必须熟记

这些算法不仅是考试重点,更是实际工程中的常用工具。从地图导航到网络路由,从电路设计到项目管理,处处都有它们的身影。

下一篇文章,我们将进入查找算法的世界,探讨**《查找算法(上):线性查找与树形查找》**,学习从顺序查找到B树、B+树的各种查找技术,这同样是408的必考内容。

💪 学习建议:这部分算法较多,建议分块练习,每个算法都要能手工模拟。特别是Dijkstra和Prim/Kruskal,几乎年年必考。记住:理解原理,多做真题,算法并不难!

相信自己,图论虽复杂,但掌握规律后你会发现其优美之处。加油,未来的工程师!🚀

http://www.xdnf.cn/news/18428.html

相关文章:

  • 盲盒商城h5源码搭建可二开幸运盲盒回收转增定制开发教程
  • 整体设计 之定稿 “凝聚式中心点”原型 --整除:智能合约和DBMS的在表层挂接 能/所 依据的深层套接
  • YAML格式笔记
  • Linux-----《Linux系统管理速通:界面切换、远程连接、目录权限与用户管理一网打尽》
  • Download:几款主流的全球范围的NDVI产品参数说明和下载
  • 领码方案:通用物联网数据采集低代码集成平台——万物智联时代的黄金钥匙
  • 电梯RFID楼层状态采集器的功能需求及参数要求,以下为多奥综合技术解析与参数说明,整合了十几年项目相关技术指标及应用场景:
  • 【java面试day16】mysql-覆盖索引
  • 签名应用APP分发平台的微服务化部署是什么?其有哪些优势?
  • LoRa 网关与节点组网方案
  • Linux I/O 多路复用实战:Select/Poll 编程指南
  • ansible中roles角色是什么意思?
  • 2026 年越南未来能源展
  • 数据清洗(Data Cleansing)——机器学习深度学习所需数据的清洗实战案例 (结构清晰、万字解析、完整代码)包括机器学习方法预测缺失值的实践
  • webrtc弱网-GoogCcNetworkController类源码分析与算法原理
  • MyBatis-Plus基础篇详解
  • Kubernetes 的 YAML 配置文件-kind
  • vue3+element-plus 输入框el-input设置背景颜色和字体颜色,样式效果等同于不可编辑的效果
  • ubuntu24.04 用apt安装的mysql修改存储路径(文件夹、目录)
  • 【CUDA教程--3】通过简单的矩阵运算入门CUDA
  • C# NX二次开发:操作按钮控件Button和标签控件Label详解
  • 华为鸿蒙系统SSH如何通过私钥连接登录
  • RadioIrqProcess函数详细分析与流程图
  • for-else 流程控制结构介绍
  • 3、栈和队列
  • LG P3710 方方方的数据结构 Solution
  • 指针的应用学习日记
  • 算法训练营day55 图论⑤ 并查集理论基础、107. 寻找存在的路径
  • 信号和共享内存
  • Linux------《零基础到联网:CentOS 7 在 VMware Workstation 中的全流程安装与 NAT 网络配置实战》