拓扑排序:DFS、BFS 与环检测
介绍有向无环图的拓扑排序,分别使用 DFS 和 Kahn 算法实现,并说明如何检测有向环。
拓扑排序是对有向图顶点进行线性排列的一种方法。对于图中的每条边 u -> v,拓扑序都必须保证 u 出现在 v 之前。
只有有向无环图(DAG)一定存在拓扑排序。如果图中存在环,环上的顶点无法满足彼此的先后约束。
一、使用 DFS 求拓扑排序
DFS 版本需要记录每个顶点的访问状态:
0:还没有访问。1:正在当前递归路径中访问。2:已经访问完成。
当访问边 u -> v 时,如果 v 的状态为 1,说明从 v 可以沿当前递归路径回到 u,因此发现了有向环。
#include <algorithm>
#include <utility>
#include <vector>
bool Dfs(int vertex,
const std::vector<std::vector<int>>& graph,
std::vector<int>& state,
std::vector<int>& order) {
state[vertex] = 1;
for (int next : graph[vertex]) {
if (state[next] == 1) {
return false; // 找到有向环
}
if (state[next] == 0 &&
!Dfs(next, graph, state, order)) {
return false;
}
}
state[vertex] = 2;
order.push_back(vertex); // 后序加入
return true;
}
std::vector<int> TopologicalSortByDfs(
int vertex_count, const std::vector<std::pair<int, int>>& edges) {
std::vector<std::vector<int>> graph(vertex_count);
for (const auto& [from, to] : edges) {
graph[from].push_back(to);
}
std::vector<int> state(vertex_count, 0);
std::vector<int> order;
order.reserve(vertex_count);
for (int vertex = 0; vertex < vertex_count; ++vertex) {
if (state[vertex] == 0 &&
!Dfs(vertex, graph, state, order)) {
return {}; // 空结果表示存在环
}
}
std::reverse(order.begin(), order.end());
return order;
}
顶点在所有后继顶点完成后才加入 order,因此得到的是后序排列;将后序排列反转后,就是拓扑序。
DFS 的时间复杂度为 O(V + E),空间复杂度为 O(V + E)。递归深度很大的图可能导致栈空间不足,此时可以使用 BFS 版本,或自行实现显式栈。
二、使用 BFS 求拓扑排序
Kahn 算法的核心是入度:
- 统计每个顶点的入度。
- 将所有入度为
0的顶点加入队列。 - 取出队首顶点,将它加入结果,并把所有出边指向顶点的入度减
1。 - 新变为入度
0的顶点加入队列。
#include <optional>
#include <queue>
#include <utility>
#include <vector>
std::optional<std::vector<int>> TopologicalSortByBfs(
int vertex_count, const std::vector<std::pair<int, int>>& edges) {
std::vector<std::vector<int>> graph(vertex_count);
std::vector<int> indegree(vertex_count, 0);
for (const auto& [from, to] : edges) {
graph[from].push_back(to);
++indegree[to];
}
std::queue<int> pending;
for (int vertex = 0; vertex < vertex_count; ++vertex) {
if (indegree[vertex] == 0) {
pending.push(vertex);
}
}
std::vector<int> order;
order.reserve(vertex_count);
while (!pending.empty()) {
const int vertex = pending.front();
pending.pop();
order.push_back(vertex);
for (int next : graph[vertex]) {
if (--indegree[next] == 0) {
pending.push(next);
}
}
}
if (static_cast<int>(order.size()) != vertex_count) {
return std::nullopt; // 存在有向环
}
return order;
}
如果队列为空时仍有顶点没有加入结果,说明这些顶点属于某个环,或者依赖于环中的顶点,因此不存在完整的拓扑序。
同一个图通常可以有多个合法的拓扑序。若题目要求字典序最小的拓扑序,可以把普通队列替换为最小堆。
三、DFS 和 BFS 的比较
| 方法 | 主要依据 | 环检测方式 | 适合场景 |
|---|---|---|---|
| DFS | 后序遍历 | 发现指向递归栈的边 | 递归表达自然、需要沿依赖深入 |
| BFS / Kahn | 顶点入度 | 结果顶点数小于 V |
迭代实现、需要逐层处理入度 |
两种方法的时间复杂度都是 O(V + E)。
四、常见题目
五、总结
- 拓扑排序只适用于有向无环图。
- DFS 使用逆后序遍历得到拓扑序,并通过递归栈检测环。
- BFS 使用入度为
0的顶点逐步删除依赖,并通过结果数量检测环。 - 如果无法处理全部顶点,图中一定存在有向环。