一、什么是拓撲排序?
在圖論中,拓撲排序(Topological Sorting)是一個有向無環圖(DAG, Directed Acyclic Graph)的所有頂點的線性序列。且該序列必須滿足下面兩個條件:
- 每個頂點出現且只出現一次。
- 若存在一條從頂點 A 到頂點 B 的路徑,那么在序列中頂點 A 出現在頂點 B 的前面。
有向無環圖(DAG)才有拓撲排序,非DAG圖沒有拓撲排序一說。
拓撲排序其實還是挺奇妙的,就是解決誰先誰后的問題
例如,下面這個圖:

-
從 DAG 圖中選擇一個沒有前驅(即入度為0)的頂點並輸出。
-
從圖中刪除該頂點和所有以它為起點的有向邊。
-
重復 1 和 2 直到當前的 DAG 圖為空或當前圖中不存在無前驅的頂點為止。

通常,一個有向無環圖可以有一個或多個拓撲排序序列。這是因為可能同時存在多個入度為0的結點,這時,先處理哪個都是可以的。
二. 拓撲排序用來干什么?
- 判斷是否有環
- 求DAG中的最長鏈
拓撲排序有兩種方式,就是bfs和dfs,一般書中介紹的大多數是bfs,大家就以為拓撲排序只有一種辦法,其實是不對的。
參考鏈接 :https://blog.csdn.net/weixin_43918531/article/details/86740991
三、拓撲排序的bfs模板
有一個明確的思路:每一個頂點都有入度和出度,入度為0說明沒有指向他的,那么就從他開始往下找。把這個入度為0的push進隊列(還要注意保存入度為0的點),同時把與這個點相連的所有點的入度-1,然后再看看有沒有入度為0的,有的話繼續push,循環上面的操作,直到沒有入度為0的點。
看一下上面的圖,如果從序號1開始的話:1入度為0,push進隊列,頂點2的入度-1,所以頂點2push進隊列,3和5的入度-1,3push進隊列,5push進隊列,頂點3進隊列后,頂點4入度-1,頂點4push進隊列,所以輸出結果就是1 2 3 5 4

#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
//本代碼功能:以bfs輸出一個有向無環圖DAG的拓撲序
const int N = 1010;
vector<int> edge[N]; //鄰接表
int ind[N]; //入度數組
queue<int> q; //隊列
int n; //n個結點
int m; //m條邊
int main() {
//讀入,建圖
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
edge[x].push_back(y);
ind[y]++;//維護入度
}
//入度為零的放入隊列
for (int i = 1; i <= n; i++) if (!ind[i]) q.push(i);
//廣度優先搜索DAG,就是拓撲排序的模板
while (!q.empty()) {
int x = q.front();
q.pop();
//輸出拓撲序
cout << x << " ";
for (int i = 0; i < edge[x].size(); i++) { //遍歷所有出邊
int y = edge[x][i]; //目標結點
//對接點入度-1,抹去這條入邊
ind[y]--;
//如果入度為0,則入隊列,准備處理它
if (!ind[y]) q.push(y);
}
}
return 0;
}
/**
測試數據
5 4
1 2
2 3
2 5
3 4
參考答案:
1 2 5 3 4
*/
四、拓撲排序的dfs模板

DFS是從一個點不斷往下遞歸,比如說從序號1往下遞歸,有箭頭就一直往下進行,直到到了最后一個元素,就開始往棧里(當然也可以是vector之類的,只不過需要反向再輸出)push元素。比如說上面的從序號1開始,到序號2,序號3,序號4,到盡頭了,就把4push進棧中,3push進棧,這個時候由於5也是2的下一個元素,所以5push進棧中,2push進棧,1push進棧,然后輸出就是1 2 5 3 4.
當然這個遞歸的順序是與你輸入的順序有關的,不過思路都是這樣的,由起始點向下遞歸。
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
//本代碼功能:以dfs輸出一個有向無環圖DAG的拓撲序
const int N = 1010;
bool st[N]; //標識是不是已經使用過
vector<int> edge[N]; //鄰接表
vector<int> res; //拓撲序列
/**
* 功能:深度優先搜索,記錄拓撲序
* @param u
*/
void dfs(int u) {
//如果訪問過了,則返回,不再重復訪問
if (st[u])return;
//標識u結點已使用
st[u] = true;
//遍歷每個出邊,找到下一組結點
for (int v:edge[u]) if (!st[v]) dfs(v);
//這一層完畢才把它自己扔進去,最后扔等於最先輸出,因為后面是倒序輸出的
res.push_back(u);
}
int n; //n個結點
int m; //m條邊
int main() {
//讀入,建圖
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
edge[x].push_back(y);
}
//將所有結點進行深入優先搜索
for (int i = 1; i <= n; i++) dfs(i);
//輸出,從后向前輸出
for (int i = res.size() - 1; i >= 0; i--)
cout << res[i] << " ";
}
/**
測試數據
5 4
1 2
2 3
2 5
3 4
參考答案:
1 2 5 3 4
*/
五、dfs判斷是否有環
#include <bits/stdc++.h>
using namespace std;
/**
5 5
1 2
2 3
2 5
3 4
3 1
對比上個例子,添加了一條3->1的邊,就成了有向有環圖.
本題,也就不能輸出拓撲序了,因為有環圖沒有拓撲序,拓撲序是針對DAG的。可以判斷是否有環。
*/
//本代碼功能:以dfs判斷一個有向圖SDG是否有環
const int N = 1010;
int st[N]; //標識是不是已經使用過
vector<int> edge[N];//鄰接表
int n; //n個結點
int m; //m個關系
/**
* 功能:深度優先搜索,判斷以u開頭的圖中是否有環,有環:true,無環:false
有向有環圖dfs判斷是否有環只需要把st[]的狀態改一下,原本是兩種狀態,0和1,
現在改成 0,1,-1
0:代表未訪問
-1:代表訪問完畢
1:代表是這一階段正在訪問的(這一階段指的是兩個元素在同一個遞歸中)。
*/
bool dfs(int u) {
//標識u結點正在訪問
st[u] = 1;
//遍歷每個出邊,找到下一組結點
for (int v:edge[u]) {
//如果遇到了正在訪問的結點,那么說明有環
if (st[v] == 1) return true;
//如果v這個結點沒有訪問過,遞歸查找v結點是否在環中
if (st[v] == 0 && dfs(v)) return true;
}
//標識u結點訪問完畢
st[u] = -1;
return false;
}
int main() {
//讀入,建圖
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
edge[x].push_back(y);
}
//將所有未訪問過的結點進行深入優先搜索判斷是否有環
for (int i = 1; i <= n; i++)
if (!st[i] && dfs(i))//沒有訪問過,並且有環
cout << "發現環!" << endl;
return 0;
}
六、bfs判斷是否有環
#include <bits/stdc++.h>
using namespace std;
/**
5 5
1 2
2 3
2 5
3 4
3 1
對比上個例子,添加了一條3->1的邊,就成了有向有環圖.
本題,也就不能輸出拓撲序了,因為有環圖沒有拓撲序,拓撲序是針對DAG的。可以判斷是否有環。
*/
//本代碼功能:以bfs判斷一個有向圖SDG是否有環
const int N = 1010;
int st[N]; //標識是不是已經使用過
vector<int> edge[N];//鄰接表
int ind[N]; //入度表
int n; //n個結點
int m; //m個關系
queue<int> q; //隊列
vector<int> ans; //ans 為拓撲序列
int main() {
//讀入,建圖
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
edge[x].push_back(y);
ind[y]++;
}
for (int i = 1; i <= n; i++) if (ind[i] == 0) q.push(i); //將入度為0的點入隊列
while (!q.empty()) {
int p = q.front();
q.pop(); // 選一個入度為0的點,出隊列
ans.push_back(p);
for (int i = 0; i < edge[p].size(); i++) {
int y = edge[p][i];
ind[y]--;
if (ind[y] == 0) q.push(y);
}
}
if (ans.size() == n) {
for (int i = 0; i < ans.size(); i++)
printf("%d ", ans[i]);
printf("\n");
} else printf("No Answer!\n"); // ans 中的長度與n不相等,就說明無拓撲序列
return 0;
}