🌟 一、什么是拓扑排序?
✅ 定义
拓扑排序是对有向无环图(DAG, Directed Acyclic Graph) 的顶点进行线性排序,使得对于图中的每一条有向边 u → v,在排序结果中,顶点 u 都出现在顶点 v 之前。
换句话说:所有“前驱”必须排在“后继”前面。
✅ 举个生活中的例子:课程学习顺序
假设你是一名大学生,有以下课程和先修要求:
| 课程 | 先修课程 |
|---|---|
| C1 | 无 |
| C2 | C1 |
| C3 | C1 |
| C4 | C2, C3 |
我们可以画出一个有向图:
C1 → C2
↓ ↓
C3 → C4
箭头表示“必须先学”。
那么一个合法的拓扑排序是:C1 → C2 → C3 → C4 或 C1 → C3 → C2 → C4,但不能是 C2 → C1 → ...,因为违反了依赖。
✅ 为什么必须是“有向无环图”?
- 有向:因为依赖关系是有方向的(先修课 → 当前课)。
- 无环:如果有环,比如 A → B → A,就形成死循环,无法排序。
❌ 举例:如果 C2 要求先修 C3,而 C3 又要求先修 C2,这就不可能完成,图中有环,无法拓扑排序。
🌟 二、图的表示方式
在 C++ 中,我们通常用邻接表(Adjacency List) 来表示图:
vector<vector<int>> adj;
adj[u]是一个 vector,存储所有从u出发的边指向的顶点v。- 例如:
adj[5] = {2, 0}表示顶点 5 有边指向 2 和 0。
🌟 三、拓扑排序的核心思想:Kahn 算法(剥洋葱法)
🔍 算法流程(详细步骤)
我们用一个“任务依赖”图来演示:
5 → 2 → 3 → 1
↓ ↗
0 4 → 1
更清晰地表示依赖关系:
- 2 依赖 5
- 3 依赖 2
- 1 依赖 3 和 4
- 0 依赖 5
- 1 依赖 4
步骤 1:计算每个顶点的入度(in-degree)
| 顶点 | 入度 | 说明 |
|---|---|---|
| 0 | 1 | 被 5 指向 |
| 1 | 2 | 被 3 和 4 指向 |
| 2 | 1 | 被 5 指向 |
| 3 | 1 | 被 2 指向 |
| 4 | 1 | 题目中是 4→1,4 的入度是 0 |
我们根据 addEdge 调用:
g.addEdge(5, 2); // 5→2
g.addEdge(5, 0); // 5→0
g.addEdge(4, 0); // 4→1
原代码中是:
g.addEdge(4, 0);
g.addEdge(4, 1);
所以:
- 0 被 5 和 4 指向 → 入度=2
- 1 被 4 和 3 指向 → 入度=2
- 2 被 5 指向 → 入度=1
- 3 被 2 指向 → 入度=1
- 4 没有被指向 → 入度=0
- 5 没有被指向 → 入度=0
所以初始入度:
| 顶点 | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| 入度 | 2 | 2 | 1 | 1 | 0 | 0 |
入度为0的顶点:4 和 5
步骤 2:将入度为0的顶点加入队列
队列 q = [4, 5](顺序取决于遍历顺序)
步骤 3:开始处理队列
我们一步步模拟:
🔁 第1轮:取出 4
-
加入结果:
topo = [4] -
遍历 4 的邻接点:
adj[4] = {0, 1}- 处理 0:
inDegree[0]--→ 2→1,仍不为0 - 处理 1:
inDegree[1]--→ 2→1,仍不为0
- 处理 0:
队列现在:[5]
🔁 第2轮:取出 5
-
加入结果:
topo = [4, 5] -
遍历 5 的邻接点:
adj[5] = {2, 0}- 处理 2:
inDegree[2]--→ 1→0 → 入队! - 处理 0:
inDegree[0]--→ 1→0 → 入队!
- 处理 2:
队列现在:[2, 0]
🔁 第3轮:取出 2
-
加入结果:
topo = [4, 5, 2] -
遍历 2 的邻接点:
adj[2] = {3}- 处理 3:
inDegree[3]--→ 1→0 → 入队!
- 处理 3:
队列现在:[0, 3]
🔁 第4轮:取出 0
- 加入结果:
topo = [4, 5, 2, 0] adj[0]为空 → 无操作
队列现在:[3]
🔁 第5轮:取出 3
-
加入结果:
topo = [4, 5, 2, 0, 3] -
遍历 3 的邻接点:
adj[3] = {1}- 处理 1:
inDegree[1]--→ 1→0 → 入队!
- 处理 1:
队列现在:[1]
🔁 第6轮:取出 1
- 加入结果:
topo = [4, 5, 2, 0, 3, 1] adj[1]为空 → 结束
队列为空。
✅ 最终结果:[4, 5, 2, 0, 3, 1]
验证依赖关系:
- 5→2:5 在 2 前 ✅
- 5→0:5 在 0 前 ✅
- 4→0:4 在 0 前 ✅
- 4→1:4 在 1 前 ✅
- 2→3:2 在 3 前 ✅
- 3→1:3 在 1 前 ✅
全部满足!排序成功。
🌟 四、C++ 代码逐行详解
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
vector:存储邻接表和结果queue:用于 Kahn 算法的 BFS 过程
class Graph {
private:
int V;
vector<vector<int>> adj; // adj[u] 存储 u 指向的所有 v
vector<int> inDegree; // inDegree[i] 表示顶点 i 的入度
V:顶点数量adj:邻接表inDegree:入度数组,初始化为0
public:
Graph(int vertices) {
V = vertices;
adj.resize(V); // 调整大小为 V 个 vector
inDegree.resize(V, 0); // 初始化入度为0
}
- 构造函数:初始化图结构
void addEdge(int u, int v) {
adj[u].push_back(v); // u → v
inDegree[v]++; // v 的入度 +1
}
- 添加有向边
u → v - 注意:只影响
v的入度
vector<int> topologicalSort() {
queue<int> q;
vector<int> topoOrder;
// 找出所有入度为0的顶点
for (int i = 0; i < V; i++) {
if (inDegree[i] == 0) {
q.push(i);
}
}
- 初始化队列:所有“没有依赖”的任务先入队
while (!q.empty()) {
int u = q.front();
q.pop();
topoOrder.push_back(u); // u 可以执行了
// 遍历 u 的所有后继 v
for (int v : adj[u]) {
inDegree[v]--; // v 的入度减1(因为 u 已完成)
if (inDegree[v] == 0) {
q.push(v); // 如果 v 的依赖全完成,入队
}
}
}
- 核心循环:BFS 风格处理
- 每完成一个任务
u,就通知所有依赖它的任务v:“我完成了,你的依赖少一个”
// 检查是否所有顶点都被排序
if (topoOrder.size() != V) {
cout << "图中存在环!" << endl;
return {};
}
return topoOrder;
}
- 如果结果长度 ≠ 顶点数,说明有环(某些顶点永远无法入度为0)
🌟 五、为什么 Kahn 算法能检测环?
- 在有环的图中,环上的顶点永远无法入度为0。
- 因为每个顶点都依赖环中前一个顶点,形成死锁。
- 所以最终
topoOrder.size() < V,即可判断有环。
🌟 六、其他实现方式:DFS 法
原理:
- 对每个未访问的顶点进行 DFS。
- 当一个顶点的所有邻接点都被访问后,将该顶点压入栈。
- 最后栈中元素从顶到底就是拓扑序(或反过来输出)。
void dfs(int u, vector<bool>& visited, stack<int>& st) {
visited[u] = true;
for (int v : adj[u]) {
if (!visited[v]) {
dfs(v, visited, st);
}
}
st.push(u); // 所有后继处理完后再入栈
}
优点:代码简洁。
缺点:不如 Kahn 直观,且检测环需要额外标记(如递归栈)。
🌟 七、常见变种问题
-
输出字典序最小的拓扑序?
- 使用优先队列(最小堆) 代替普通队列。
-
判断是否有唯一拓扑序?
- 如果在任何时刻队列中有多个元素,则拓扑序不唯一。
-
求最长路径(关键路径)?
- 在拓扑排序基础上进行动态规划。
🌟 八、总结:关键点
| 要点 | 说明 |
|---|---|
| 适用图 | 有向无环图(DAG) |
| 核心思想 | 剥洋葱:从入度为0的节点开始 |
| 数据结构 | 队列 + 邻接表 + 入度数组 |
| 时间复杂度 | O(V + E) |
| 空间复杂度 | O(V + E) |
| 结果唯一性 | 不唯一,取决于入队顺序 |
| 环检测 | 结果长度 < V → 有环 |
✅ 最后一句话总结:
拓扑排序 = 找出一个满足所有“先决条件”的执行顺序。
就像做菜:先买菜 → 洗菜 → 切菜 → 炒菜 → 上桌。每一步都依赖前一步,拓扑排序就是帮你安排这个顺序。
444

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



