文章目录
- [题型:拓扑排序(Topological Sort)](#题型:拓扑排序(Topological Sort))
-
- [1. 核心思路](#1. 核心思路)
-
- [1.1 基本概念](#1.1 基本概念)
- [1.2 适用场景](#1.2 适用场景)
- [1.3 核心思想](#1.3 核心思想)
- [2. 模板](#2. 模板)
-
- [2.1 Kahn算法(BFS方法,推荐)](#2.1 Kahn算法(BFS方法,推荐))
- [2.2 DFS方法](#2.2 DFS方法)
- [2.3 时间复杂度分析](#2.3 时间复杂度分析)
- [3. 常见变形](#3. 常见变形)
-
- [3.1 判断有向图是否有环](#3.1 判断有向图是否有环)
- [3.2 求所有可能的拓扑排序](#3.2 求所有可能的拓扑排序)
- [3.3 字典序最小的拓扑排序](#3.3 字典序最小的拓扑排序)
- [3.4 分层拓扑排序](#3.4 分层拓扑排序)
- [3.5 最长路径(关键路径)](#3.5 最长路径(关键路径))
- [4. 典型应用场景](#4. 典型应用场景)
- [5. Kahn算法 vs DFS方法](#5. Kahn算法 vs DFS方法)
- [6. 注意事项](#6. 注意事项)
题型:拓扑排序(Topological Sort)
1. 核心思路
拓扑排序是对**有向无环图(DAG, Directed Acyclic Graph)**的顶点进行线性排序,使得对于图中的每一条有向边 (u, v),u 在排序中都出现在 v 之前。
1.1 基本概念
- 有向无环图(DAG):有向图中不存在环的图
- 拓扑排序:将DAG的所有顶点排成线性序列,满足所有有向边的方向性
- 入度(In-degree):指向该顶点的边的数量
- 出度(Out-degree):从该顶点出发的边的数量
示例:
有向图: 拓扑排序结果:
A → B → D A → B → C → D
↓ ↓ 或
C E A → C → B → D → E
↓
D
1.2 适用场景
- 课程表问题:先修课程必须在后续课程之前完成
- 任务调度:某些任务必须在其他任务之前执行
- 依赖关系:编译顺序、软件包安装顺序
- 判断有向图是否有环:如果拓扑排序结果包含所有节点,则无环;否则有环
1.3 核心思想
拓扑排序的核心思想是从入度为0的节点开始,逐步移除节点并更新其他节点的入度。
算法流程:
- 初始化:统计所有节点的入度
- 找起点:将所有入度为0的节点加入队列
- 处理节点 :
- 从队列中取出一个节点,加入结果
- 将该节点指向的所有节点的入度减1
- 如果某个节点的入度变为0,将其加入队列
- 判断:如果结果集中节点数等于图中节点数,则拓扑排序成功;否则图中有环
图解过程:
初始图: 步骤1:A入度为0 步骤2:处理A后 步骤3:处理B后
A → B → D A入队 B入度变为1 C入度变为0
↓ ↓ B入度=1 C入度=1 C入队
C E C入度=1 D入度=1 D入度=1
↓ D入度=2 E入度=1 E入度=1
D E入度=1
最终结果:A → B → C → D → E
2. 模板
2.1 Kahn算法(BFS方法,推荐)
基于入度的拓扑排序,使用队列实现:
cpp
#include<iostream>
#include<vector>
#include<queue>
#include<unordered_map>
using namespace std;
int main(){
int n, m, s, t; // n是节点数,m是边数
cin >> n >> m;
vector<int> inDegree(n, 0); // 记录每个节点的入度
unordered_map<int, vector<int>> graph; // 记录图的邻接关系
vector<int> result; // 记录拓扑排序结果
// 读入边,构建图和入度数组
while(m--){
cin >> s >> t; // s指向t,即s是t的前驱
inDegree[t]++; // t的入度加1
graph[s].push_back(t); // s指向t
}
queue<int> que;
// 将所有入度为0的节点加入队列
for(int i = 0; i < n; i++){
if(inDegree[i] == 0) {
que.push(i);
}
}
// BFS处理
while(!que.empty()){
int cur = que.front(); // 取出队首节点
que.pop();
result.push_back(cur); // 加入结果
// 遍历cur指向的所有节点
vector<int> neighbors = graph[cur];
for(int i = 0; i < neighbors.size(); i++){
int next = neighbors[i];
inDegree[next]--; // 入度减1
if(inDegree[next] == 0) { // 如果入度变为0,加入队列
que.push(next);
}
}
}
// 输出结果
if(result.size() == n){
// 拓扑排序成功,输出结果
for(int i = 0; i < n - 1; i++) {
cout << result[i] << " ";
}
cout << result[n - 1] << endl;
} else {
// 图中有环,无法完成拓扑排序
cout << -1 << endl;
}
return 0;
}
2.2 DFS方法
基于深度优先搜索的拓扑排序:
cpp
#include<iostream>
#include<vector>
#include<unordered_map>
using namespace std;
vector<int> result;
vector<int> visited; // 0:未访问, 1:正在访问, 2:已访问
bool dfs(int node, unordered_map<int, vector<int>>& graph) {
if(visited[node] == 1) return false; // 发现环
if(visited[node] == 2) return true; // 已处理过
visited[node] = 1; // 标记为正在访问
// 递归处理所有邻居
for(int neighbor : graph[node]) {
if(!dfs(neighbor, graph)) {
return false; // 发现环
}
}
visited[node] = 2; // 标记为已访问
result.push_back(node); // 后序遍历,最后加入结果
return true;
}
int main(){
int n, m, s, t;
cin >> n >> m;
unordered_map<int, vector<int>> graph;
visited.resize(n, 0);
while(m--){
cin >> s >> t;
graph[s].push_back(t);
}
// 对所有未访问的节点进行DFS
for(int i = 0; i < n; i++){
if(visited[i] == 0) {
if(!dfs(i, graph)) {
cout << -1 << endl; // 有环
return 0;
}
}
}
// 反转结果(DFS是后序遍历,需要反转)
reverse(result.begin(), result.end());
for(int i = 0; i < n - 1; i++) {
cout << result[i] << " ";
}
cout << result[n - 1] << endl;
return 0;
}
2.3 时间复杂度分析
- Kahn算法(BFS) :
- 时间复杂度:O(V + E),每个节点和边访问一次
- 空间复杂度:O(V),队列和入度数组
- DFS方法 :
- 时间复杂度:O(V + E)
- 空间复杂度:O(V),递归栈深度
3. 常见变形
3.1 判断有向图是否有环
拓扑排序的副产品,如果结果集中节点数小于图中节点数,则存在环:
cpp
bool hasCycle(int n, vector<vector<int>>& edges) {
vector<int> inDegree(n, 0);
vector<vector<int>> graph(n);
// 构建图和入度
for(auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
inDegree[edge[1]]++;
}
queue<int> que;
for(int i = 0; i < n; i++) {
if(inDegree[i] == 0) que.push(i);
}
int count = 0;
while(!que.empty()) {
int cur = que.front();
que.pop();
count++;
for(int next : graph[cur]) {
inDegree[next]--;
if(inDegree[next] == 0) {
que.push(next);
}
}
}
return count != n; // 如果count < n,说明有环
}
3.2 求所有可能的拓扑排序
使用回溯法枚举所有可能的拓扑排序:
cpp
vector<vector<int>> allTopologicalSorts(int n, vector<vector<int>>& edges) {
vector<vector<int>> graph(n);
vector<int> inDegree(n, 0);
for(auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
inDegree[edge[1]]++;
}
vector<vector<int>> result;
vector<int> path;
vector<bool> visited(n, false);
function<void()> backtrack = [&]() {
if(path.size() == n) {
result.push_back(path);
return;
}
for(int i = 0; i < n; i++) {
if(!visited[i] && inDegree[i] == 0) {
visited[i] = true;
path.push_back(i);
// 更新入度
for(int next : graph[i]) {
inDegree[next]--;
}
backtrack();
// 回溯
for(int next : graph[i]) {
inDegree[next]++;
}
path.pop_back();
visited[i] = false;
}
}
};
backtrack();
return result;
}
3.3 字典序最小的拓扑排序
使用优先队列(小根堆)保证字典序最小:
cpp
vector<int> topologicalSortLexicographically(int n, vector<vector<int>>& edges) {
vector<vector<int>> graph(n);
vector<int> inDegree(n, 0);
for(auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
inDegree[edge[1]]++;
}
priority_queue<int, vector<int>, greater<int>> pq; // 小根堆
for(int i = 0; i < n; i++) {
if(inDegree[i] == 0) pq.push(i);
}
vector<int> result;
while(!pq.empty()) {
int cur = pq.top();
pq.pop();
result.push_back(cur);
for(int next : graph[cur]) {
inDegree[next]--;
if(inDegree[next] == 0) {
pq.push(next);
}
}
}
return result;
}
3.4 分层拓扑排序
按拓扑排序的层次输出结果:
cpp
vector<vector<int>> levelTopologicalSort(int n, vector<vector<int>>& edges) {
vector<vector<int>> graph(n);
vector<int> inDegree(n, 0);
for(auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
inDegree[edge[1]]++;
}
queue<int> que;
for(int i = 0; i < n; i++) {
if(inDegree[i] == 0) que.push(i);
}
vector<vector<int>> levels;
while(!que.empty()) {
int size = que.size();
vector<int> level;
for(int i = 0; i < size; i++) {
int cur = que.front();
que.pop();
level.push_back(cur);
for(int next : graph[cur]) {
inDegree[next]--;
if(inDegree[next] == 0) {
que.push(next);
}
}
}
levels.push_back(level);
}
return levels;
}
3.5 最长路径(关键路径)
在DAG中,拓扑排序可以用于计算最长路径:
cpp
int longestPath(int n, vector<vector<int>>& edges) {
vector<vector<int>> graph(n);
vector<int> inDegree(n, 0);
vector<int> dist(n, 0); // 记录到每个节点的最长路径
for(auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
inDegree[edge[1]]++;
}
queue<int> que;
for(int i = 0; i < n; i++) {
if(inDegree[i] == 0) {
que.push(i);
dist[i] = 1; // 起点距离为1
}
}
while(!que.empty()) {
int cur = que.front();
que.pop();
for(int next : graph[cur]) {
dist[next] = max(dist[next], dist[cur] + 1);
inDegree[next]--;
if(inDegree[next] == 0) {
que.push(next);
}
}
}
return *max_element(dist.begin(), dist.end());
}
4. 典型应用场景
-
课程表问题
- LeetCode 207. 课程表(判断是否可以完成)
- LeetCode 210. 课程表 II(输出学习顺序)
- 判断先修课程关系是否合理
-
任务调度问题
- 项目任务依赖关系
- 编译顺序问题
- 软件包安装顺序
-
依赖关系问题
- 文件依赖关系
- 模块依赖关系
- 事件先后顺序
-
判断有向图是否有环
- 检测依赖关系中的循环依赖
- 验证任务调度的可行性
-
关键路径问题
- 项目管理中的关键路径分析
- 最长路径计算
5. Kahn算法 vs DFS方法
| 比较项 | Kahn算法(BFS) | DFS方法 |
|---|---|---|
| 实现方式 | 队列 + 入度统计 | 递归 + 三色标记 |
| 代码复杂度 | 简单 | 中等 |
| 空间复杂度 | O(V) | O(V)(递归栈) |
| 时间复杂度 | O(V+E) | O(V+E) |
| 优势 | 实现简单,易于理解 | 可以检测环的具体位置 |
| 劣势 | 需要额外空间存储入度 | 递归可能栈溢出 |
| 推荐 | 推荐使用 | 特殊场景使用 |
选择建议:
- 一般情况 → 使用 Kahn算法(BFS),实现简单,效率高
- 需要检测环的位置 → 使用 DFS方法
- 需要所有拓扑排序 → 使用 回溯法
6. 注意事项
-
只适用于有向无环图(DAG)
- 如果图中有环,拓扑排序无法完成
- 可以通过结果集大小判断是否有环
-
拓扑排序结果不唯一
- 一个DAG可能有多个有效的拓扑排序
- 如果需要字典序最小,使用优先队列
-
入度统计要准确
- 确保正确统计每个节点的入度
- 注意边的方向(s → t 表示s是t的前驱)
-
处理多个连通分量
- 如果图不连通,需要对每个连通分量分别处理
- 或者使用DFS方法统一处理