拓撲排序 (DFS和BFS及判斷是否有環)
一、什麼是拓撲排序?
在圖論中,拓撲排序(Topological Sorting)是一個有向無環圖(DAG, Directed Acyclic Graph)的所有頂點的線性序列。且該序列必須滿足下面兩個條件:
- 每個頂點出現且只出現一次。
- 若存在一條從頂點 A 到頂點 B 的路徑,那麼在序列中頂點 A 出現在頂點 B 的前面。
有向無環圖(DAG)才有拓撲排序,非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; }
四、拓撲排序的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
*/