最短路径与拓扑排序
最短路径与拓扑排序
最短路径三种算法
BFS(无权图)
BFS 求无权图单源最短路径利用其逐层扩展的特性:首次到达即最短。详见 图的遍历。
| 指标 | 值 |
|---|---|
| 适用场景 | 无权图(边权均为 1) |
| 时间复杂度 | O(V+E)(邻接表)/ O(V2)(邻接矩阵) |
| 数据结构 | 队列 |
| 原理 | 逐层扩展,层数即距离 |
Dijkstra 算法(非负权图)
核心思想
Dijkstra 采用贪心策略:每轮从尚未确定最短路径的顶点中选出距离源点最近的顶点,将其最短路径确定下来,然后用该顶点去松弛其邻接顶点。
维护三个数组:
dist[]:源点到各顶点的当前最短距离visited[]:标记顶点最短路径是否已确定path[]:记录前驱顶点,用于回溯完整路径
核心操作——松弛(Relaxation):
若 dist[u] + w(u,v) < dist[v],则更新 dist[v] = dist[u] + w(u,v)算法过程
- 初始化:
dist[src] = 0,其余dist[i] = INF;visited[]全部 false - 重复 V 次:
- 从未访问顶点中选出
dist最小的顶点 u,标记为已访问 - 对 u 的所有邻接顶点 v 执行松弛
- 从未访问顶点中选出
- 算法结束后
dist[]即为最短距离
复杂度与适用条件
| 实现方式 | 时间复杂度 | 适用场景 |
|---|---|---|
| 邻接矩阵 + 简单遍历 | O(V2) | 稠密图 |
| 邻接表 + 最小堆 | O(E log V) | 稀疏图 |
重要限制:Dijkstra 不能处理含负权边的图。因为一旦顶点被标记为已访问,其 dist 值就不会再更新,但后续可能通过负权边找到更短的路径。
易错:Dijkstra 不能处理负权边,但可以处理带 0 权边的图。408 选择题中”负权”和”非正权”是不同概念。
Floyd 算法(多源最短路径)
核心思想
Floyd 算法通过逐步引入中转顶点来求出任意两点间的最短路径,本质是动态规划。
状态转移方程:
dist(k)[i][j] = min(dist(k-1)[i][j], dist(k-1)[i][k] + dist(k-1)[k][j])其中 dist(k)[i][j] 表示仅允许经过顶点 0~k 作为中转时 i 到 j 的最短路径。
算法过程
- 初始化:将邻接矩阵复制到
dist[][],dist[i][i] = 0,无边则 INF - 三重循环:最外层枚举中转顶点 k,内两层枚举所有顶点对 (i, j)
- 若
dist[i][k] + dist[k][j] < dist[i][j],则更新
for (int k = 0; k < V; k++) // k 必须在最外层 for (int i = 0; i < V; i++) for (int j = 0; j < V; j++) if (dist[i][k] + dist[k][j] < dist[i][j]) dist[i][j] = dist[i][k] + dist[k][j];复杂度与适用条件
| 指标 | 值 |
|---|---|
| 时间复杂度 | O(V3) |
| 空间复杂度 | O(V2) |
| 负权边 | 支持(但不能有负权回路) |
易错:三重循环的顺序必须是 k 在最外层。如果把 k 放在内层(如 i-j-k),算法就是错的——因为松弛 dist[i][j] 时,dist[i][k] 和 dist[k][j] 可能尚未被当前轮次更新到最优值。这是 408 选择题的经典陷阱。
三种算法对比
| 对比项 | BFS | Dijkstra | Floyd |
|---|---|---|---|
| 问题类型 | 单源最短路径 | 单源最短路径 | 多源最短路径 |
| 适用图 | 无权图 | 非负权图 | 任意图(无负环) |
| 时间复杂度 | O(V+E) | O(V2) / O(E log V) | O(V3) |
| 空间复杂度 | O(V) | O(V) | O(V2) |
| 算法思想 | 逐层扩展 | 贪心 | 动态规划 |
| 负权边 | N/A | 不支持 | 支持 |
| 考研考查 | 算法设计 | 手动模拟填表 | 三重循环顺序陷阱 |
DAG 描述表达式
有向无环图(DAG, Directed Acyclic Graph)是表示含有公共子表达式的代数表达式的有效工具。
二叉树 vs DAG:二叉树中重复的子表达式存储多份;DAG 中公共子表达式只需存储一次,通过多个父结点共享。
以表达式 (x+y)((x+y)/x) 为例,DAG 只需 5 个顶点(x, y, +, /, *),而二叉树需要更多结点。
构造方法:自底向上合并重复结点——相同操作数的叶子只保留一个;运算符相同且左右子结点都相同的内部结点合并为一个。
易错:DAG 描述表达式是图论的应用,不要和”二叉树表示表达式”搞混。前者重点是共享子结构减少空间,后者重点是用栈计算表达式的值。
拓扑排序
前提与定义
拓扑排序的前提:图必须是有向无环图(DAG)。若图中存在环,则不存在拓扑序列。
- AOV 网(Activity On Vertex):用顶点表示活动、有向边表示活动间先后关系的有向图
- 拓扑序列不唯一:一个 DAG 通常有多个合法的拓扑序列
Kahn 算法(BFS 入度法)
- 计算所有顶点的入度,将所有入度为 0 的顶点入队
- 队列非空时,取出队首顶点并输出
- 将该顶点的所有邻接顶点入度减 1,若入度变为 0 则入队
- 重复直到队列为空
环检测:若最终输出的顶点数 < n,则图中存在环(环中顶点入度永远不会减为 0)。
DFS 逆后序法
对每个未访问顶点执行 DFS,在顶点的所有后继都访问完毕回退时将该顶点压入栈,最终栈顶到栈底即为拓扑序列。
DFS 环检测(三色标记法):若 DFS 过程中遇到状态为”访问中”的顶点,说明存在环。
复杂度分析
| 算法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| Kahn 算法(BFS 入度法) | O(V+E) | O(V) |
| DFS 逆后序法 | O(V+E) | O(V) |
关键路径(AOE 网)
核心概念
AOE 网(Activity On Edge):用有向无环图表示工程,顶点表示事件(Event),有向边表示活动(Activity),边的权值表示活动持续时间。
- 源点:入度为 0 的顶点(工程开始)
- 汇点:出度为 0 的顶点(工程结束)
- 关键路径:从源点到汇点的最长路径,决定工程最短工期
- 关键活动:最早开始时间等于最晚开始时间的活动(时间余量为 0)
四个时间参数
| 符号 | 含义 | 计算方式 |
|---|---|---|
| ve(j) | 事件 j 的最早发生时间 | ve(j) = max{ ve(i) + w(i,j) },取所有前驱的最大值 |
| vl(j) | 事件 j 的最晚发生时间 | vl(j) = min{ vl(k) - w(j,k) },取所有后继的最小值 |
| e(i) | 活动 ai 的最早开始时间 | e(i) = ve(该活动的起点) |
| l(i) | 活动 ai 的最晚开始时间 | l(i) = vl(该活动的终点) - w(ai) |
关键活动:满足 e(i) == l(i) 的活动。
求解步骤
- 对 AOE 网进行拓扑排序,同时按拓扑序计算 ve[]
- 按逆拓扑序计算 vl[](初始化 vl[汇点] = ve[汇点])
- 遍历每条活动计算 e[] 和 l[]
- 找出所有 e(i) == l(i) 的活动,即为关键活动
关键性质
- 关键路径可能不唯一,所有关键活动构成的路径都是关键路径
- 缩短工期只能通过缩短关键活动的持续时间来实现
- 缩短某关键活动后,该路径可能不再是关键路径(其他路径变为最长),需要重新计算
- 只有缩短所有关键路径上公共的关键活动,才能确保工期缩短
易错:关键路径是最长路径,不是最短路径——这和最短路径问题的思路恰好相反。
AOV 网 vs AOE 网
| 对比项 | AOV 网 | AOE 网 |
|---|---|---|
| 顶点 | 活动 | 事件 |
| 边 | 活动间的先后关系 | 活动(带权) |
| 关注点 | 活动执行的先后顺序 | 工程的最短完成时间 |
| 核心算法 | 拓扑排序 | 关键路径 |
考研高频考点
- Dijkstra 手动模拟执行过程,填写 dist[] 表格
- Dijkstra 不能处理负权边的原因
- Floyd 三重循环顺序(k 在最外层)
- Floyd vs Dijkstra 对比
- 给定 DAG 写出所有可能的拓扑序列
- 拓扑排序判断有向图是否有环
- 手算 ve[]、vl[]、e[]、l[] 四个时间参数
- 根据 e(i) == l(i) 判断关键活动、找出关键路径
- 关键路径不唯一时缩短工期的条件分析
- AOV 网 vs AOE 网的区别
关联页面
- 图的基本概念与存储结构 — 图的存储是路径算法的基础
- 图的遍历 — BFS 求无权图最短路径
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!










