拓扑序列
拓扑排序(Topological Sorting)是处理有向无环图(DAG)的关键算法,用于确定顶点的线性顺序,使得所有有向边均从排在前面的元素指向后面的元素。以下是C++实现拓扑排序的两种经典方法及其详细解析:
一、算法原理
1. Kahn算法(基于入度的贪心算法)
- 核心思想:不断删除入度为0的顶点,直到图为空或存在环。
- 步骤:
- 统计每个顶点的入度。
- 将入度为0的顶点加入队列。
- 依次取出队列中的顶点,将其邻接顶点入度减1,若减至0则入队。
- 若结果顶点数 ≠ 总顶点数,说明存在环。
2. 基于DFS的后序遍历法
- 核心思想:通过深度优先搜索遍历图,按完成时间的逆序输出顶点。
- 步骤:
- 对未访问顶点进行DFS。
- 递归访问邻接顶点,完成后将当前顶点压入栈。
- 最终栈中元素即为拓扑序列。
- 若DFS中发现已访问但未完成的顶点,说明存在环。
二、C++代码实现
方法1:Kahn算法
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
vector<int> topologicalSortKahn(int n, vector<vector<int>>& edges) {
vector<vector<int>> adj(n);
vector<int> inDegree(n, 0);
// 构建邻接表并统计入度
for (auto& e : edges) {
adj[e[0]].push_back(e[1]);
inDegree[e[1]]++;
}
queue<int> q;
for (int i = 0; i < n; ++i) {
if (inDegree[i] == 0) q.push(i);
}
vector<int> res;
while (!q.empty()) {
int u = q.front();
q.pop();
res.push_back(u);
for (int v : adj[u]) {
if (--inDegree[v] == 0) {
q.push(v);
}
}
}
if (res.size() != n) {
cout << "图中有环!" << endl;
return {};
}
return res;
}
方法2:基于DFS的算法
#include <stack>
#include <vector>
#include <iostream>
using namespace std;
bool dfs(int u, vector<int>& visited, vector<vector<int>>& adj, stack<int>& stk) {
if (visited[u] == 1) return false; // 发现环
if (visited[u] == 2) return true; // 已处理完毕
visited[u] = 1; // 标记为正在访问
for (int v : adj[u]) {
if (!dfs(v, visited, adj, stk)) return false;
}
visited[u] = 2; // 标记为已完成
stk.push(u);
return true;
}
vector<int> topologicalSortDFS(int n, vector<vector<int>>& edges) {
vector<vector<int>> adj(n);
for (auto& e : edges) {
adj[e[0]].push_back(e[1]);
}
vector<int> visited(n, 0);
stack<int> stk;
for (int i = 0; i < n; ++i) {
if (visited[i] == 0 && !dfs(i, visited, adj, stk)) {
cout << "图中有环!" << endl;
return {};
}
}
vector<int> res;
while (!stk.empty()) {
res.push_back(stk.top());
stk.pop();
}
return res;
}
三、算法对比
特性 | Kahn算法 | DFS算法 |
---|---|---|
时间复杂度 | O(V + E) | O(V + E) |
空间复杂度 | O(V + E) | O(V)(递归栈深度) |
环检测 | 结果长度不足时立即发现 | DFS过程中发现反向边 |
适用场景 | 动态更新入度的场景 | 需要特定顺序(如字典序) |
四、使用示例
int main() {
int n = 6;
vector<vector<int>> edges = {{5,2}, {5,0}, {4,0}, {4,1}, {2,3}, {3,1}};
auto res1 = topologicalSortKahn(n, edges);
cout << "Kahn算法结果: ";
for (int v : res1) cout << v << " "; // 输出:5 4 2 3 1 0
auto res2 = topologicalSortDFS(n, edges);
cout << "\nDFS算法结果: ";
for (int v : res2) cout << v << " "; // 输出:5 4 2 3 0 1
}
五、关键点总结
- 环检测:两种算法均能检测环,Kahn通过结果长度,DFS通过递归路径。
- 顶点表示:通常用整数编号(0到n-1),便于数组索引。
- 扩展性:若需字典序最小拓扑排序,Kahn算法中将队列改为优先队列。
通过理解这两种实现方式,您可以根据实际需求选择最适合的拓扑排序策略。
六、例题
题目1
B3644 【模板】拓扑排序 / 家谱树
题目描述
有个人的家族很大,辈分关系很混乱,请你帮整理一下这种关系。给出每个人的后代的信息。输出一个序列,使得每个人的后辈都比那个人后列出。
输入格式
第 1 1 1 行一个整数 N N N( 1 ≤ N ≤ 100 1 \le N \le 100 1≤N≤100),表示家族的人数。接下来 N N N 行,第 i i i 行描述第 i i i 个人的后代编号 a i , j a_{i,j} ai,j,表示 a i , j a_{i,j} ai,j 是 i i i 的后代。每行最后是 0 0 0 表示描述完毕。
输出格式
输出一个序列,使得每个人的后辈都比那个人后列出。如果有多种不同的序列,输出任意一种即可。
输入输出样例 #1
输入 #1
5 0 4 5 1 0 1 0 5 3 0 3 0
输出 #1
2 4 5 3 1
题解
#include <iostream> #include <vector> #include <algorithm> using namespace std; // 拓扑排序函数 void topologicalSort(vector<vector<int>>& graph, vector<int>& inDegree){ auto it = find(inDegree.begin(), inDegree.end(),0); if (it != inDegree.end()){ int index = distance(inDegree.begin(), it); cout << index << " "; for (auto i: graph[index]){ inDegree[i]--; } inDegree[index] = -1; topologicalSort(graph, inDegree); } else { return; } } int main(){ int n; cin >> n; vector<vector<int>> graph(n + 1, vector<int>()); vector<int> inDegree(n + 1, 0); for (int i = 1; i <= n; ++i) { int x; while (cin >> x && x!= 0) { graph[i].push_back(x); inDegree[x]++; } } inDegree[0] = -1; topologicalSort(graph, inDegree); return 0; }
题目2
P2712 摄像头
题目描述
食品店里有 n n n 个摄像头,这种摄像头很笨拙,只能拍摄到固定位置。现有一群胆大妄为的松鼠想要抢劫食品店,为了不让摄像头拍下他们犯罪的证据,他们抢劫前的第一件事就是砸毁这些摄像头。
为了便于砸毁摄像头,松鼠歹徒们把所有摄像头和摄像头能监视到的地方统一编号,一个摄像头能被砸毁的条件是该摄像头所在位置不被其他摄像头监视。
现在你的任务是帮松鼠们计算是否可以砸掉所有摄像头,如不能则输出还没砸掉的摄像头的数量。
输入格式
第 1 1 1 行,一个整数 n n n,表示摄像头的个数。
第 2 2 2 到 n + 1 n+1 n+1 行是摄像头的信息,包括:摄像头的位置 x x x,以及这个摄像头可以监视到的位置数 m m m,之后 m m m 个数 y y y 是此摄像头可以监视到的位置。(砸了这些摄像头之后自然这些位置就监视不到了)
输出格式
若可以砸掉所有摄像头则输出“ YES \texttt{YES} YES ”,否则输出还没砸掉的摄像头的数量。(不带引号)
输入输出样例 #1
输入 #1
5 1 1 2 2 1 1 3 1 7 4 1 1 5 0
输出 #1
2
说明/提示
1 ≤ n ≤ 100 1 \leq n \leq 100 1≤n≤100。
0 ≤ m ≤ 100 0 \leq m \leq 100 0≤m≤100。
0 ≤ x , y ≤ 500 0 \leq x,y \leq 500 0≤x,y≤500。
题解
#include <iostream> #include <vector> #include <queue> using namespace std; const int MAX_POS = 501; void topoSort(vector<vector<int>>& graph, vector<int>& inDegree, int &ans, vector<bool>& isCamera) { queue<int> q; for (int i = 0; i < MAX_POS; ++i) { if (isCamera[i] && inDegree[i] == 0) { q.push(i); inDegree[i] = -1; } } while (!q.empty()) { int node = q.front(); q.pop(); ans++; for (int neighbor : graph[node]) { if (--inDegree[neighbor] == 0 && isCamera[neighbor]) { q.push(neighbor); inDegree[neighbor] = -1; } } } } int main() { int n; int ans = 0; cin >> n; vector<vector<int>> graph(MAX_POS, vector<int>()); vector<int> inDegree(MAX_POS, 0); vector<bool> isCamera(MAX_POS, false); for (int i = 0; i < n; ++i) { int pos, cnt; cin >> pos >> cnt; isCamera[pos] = true; for (int j = 0; j < cnt; ++j){ int watched_pos; cin >> watched_pos; if (watched_pos < 0 || watched_pos > 500) continue; graph[pos].push_back(watched_pos); inDegree[watched_pos]++; } } topoSort(graph, inDegree, ans, isCamera); int total = 0; for(bool cam : isCamera) if(cam) total++; if (ans == total) cout << "YES" << endl; else cout << (total - ans) << endl; return 0; }