首先來理解什麼是拓撲排序;拓撲排序簡單說是做事情的先後順序,在現實生活中,人們經常要做連串事情,這些事情之聞有順序關係或者依賴關係,在做一件事情之前必須先做另一件事,比如安排客人的座位、穿衣服的先後、課程學習的先後等。這些事情都可以抽象為圖論中的拓撲排序。
拓撲排序的概念:
設有a、b、c、d等事情,其中a有最高優先順序,b、c優先順序相同,d是最低優先順序,表示為a→(b,c)→d,那麼abcd或acbd都是可行的排序。把事情看成圖的點,把先後關係看成有向邊,問題轉化為在圖中求一個有先後關係的排序這就是拓撲排序,顯然,一個圖能進行拓撲排序的完要條件是它是一個有向無環圖(DAG)。有環圖不能進行拓撲排序,
當然,這裡還要普及兩個圖的簡單概念:
出度:從一個點為起點的出去的邊的數量為出度;
入度:以一個點為終點出去的邊的數量為出度。
從入度和長度的概念上可以看出,如果一個點的入度數量為0,那這個點必定是最前面的點,如果出度等於0,則這個點必定又是最後面的點;
同時,拓撲排序是基於bfs和dfs的思想實現的,我們就來普及一下對於bfs與dfs拓撲排序的實現方式;
1.bfs:
bfs拓撲排序是常用的拓撲排序演算法,這個演算法是基於佇列來實現的
而實現的思想,則是基於無前驅的頂點優先和無後繼的頂點優先
1.無前驅的頂點優先:
以此圖為例,
實現的步驟主要是:
(1)先找圖中入度為0的點,作為起點放進佇列,當然,這些點的先後順序是無所謂的,主要是得有,如果找完一圈都沒有發現圖紙有度為0的點,那這個圖就不是DAG圖,不存在拓撲排序;
(2)在找完圖中入度為0的節點之後,我們彈出隊首元素,並且將隊首元素的鄰居的度都減一,把度減為0的鄰居放進佇列
(3)重複以上步驟,直到佇列為空;
拿上圖來說,會輸出acbd,這就是上圖的拓撲序列;
當然,如果佇列空了,但是依舊是還有別的點沒有進入佇列,那這個圖就不DAG,也就不存在脫坡序列;
2.無後繼頂點優先:
將無前驅節點優先的執行反過來就是無後繼頂點優先的執行。
2.基於dfs的拓撲排序
dfs是天然的拓撲排序思想的體現,dfs的原理就是一條路走到黑,一直搜素到最後,然後逐層回退,正是點與點的先後關係。
一個DAG,如果只有一個點是0入度的,從這個點開始拓撲排序,返回的順序是逆序的拓撲排序,
因為遞迴返回的是最後的點,這裡就沒有後繼點了,逐層回退,最後到起點
為了得到正序的拓撲序列,資料結構中有一個專門對付逆序的線性結構---那就是棧,用棧記錄拓撲序列在輸出就可以得到正確的拓撲序列;
但是依舊是有一些細節問題:
(1)應該以入度為0的點為起點開始DFS.如何找到它?需要找到它嗎?如果有多個入度為0的點呢?
這幾個問題其實並不用特別處理。想象有一個虛擬的點 v,,它單向連線到所有其他點。這個點就是圖中唯一 的0入度點,圖中所有其他的點都是它的下一層遞迴,而且它不會把原圖變成環路。從這個虛擬點開始DFS就完成了拓撲排序。但運算的時候並不需要處理這個虛擬點,只要在主程式中把每個點輪流執行一 DFS可這樣做相當於顯式地道歸了虛點的所有下一層點,
(2)如果圖不是DAG,能判斷嗎?
圖不是DAG,說明圖是有環圖,不存在拓撲排序。(那麼在遞迴的時候會出現回退邊)
在程式中這樣發現回退邊:記錄每個點的狀態,如果dfs遞迴到某個點時發現它仍在前面的遞迴中沒有處理完畢,說明存在回退邊,不存在拓撲排序:
還要一些雜項問題:
輸出字典序最小的拓撲排序;
這種題目比如杭電1285和北大1270都有體現;
解決這裡問題,核心是在當前步驟,在所有入度為0的點中輸出編號最小的,
這裡我們不用next_permutation();
這裡採用的是優先佇列:
在bfs拓撲排序中,把普通佇列改為優先佇列,放進入度為0的節點,每次輸出最小編號........就是重複bfs拓撲排序的步驟啦
題目實戰:https://www.dotcpp.com/oj/problem1707.html
這道題前面我已發部落格:https://www.cnblogs.com/LQS-blog/p/16207985.html
來個重量級的:
https://www.acwing.com/problem/content/description/166/
友情提示,這道題不太好做,慎入;
這裡只給出程式碼,今天很晚了,以後找時間專門出篇部落格:
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int num=30010; 4 queue<int>q; 5 int n,m; 6 vector<int>edge[num];//存圖 7 vector<int>topo;//拓撲序列 8 int in[num];//入度陣列 9 bitset<num>f[num];//是標準庫中的一個固定大小序列,其儲存的資料只包含 0/1 10 int main() 11 { 12 std::ios::sync_with_stdio(false); 13 cin>>n>>m; 14 for(register int i=0;i<m;i++)//常規存圖入度操作 15 { 16 int a,b; 17 cin>>a>>b; 18 edge[a].push_back(b); 19 in[b]++; 20 } 21 for(register int i=0;i<n;i++)//把入度為0的點入隊 22 { 23 if(!in[i]) 24 q.push(i); 25 } 26 while(!q.empty()) 27 { 28 int x=q.front();//取隊首 29 q.pop(); 30 topo.push_back(x);//存入拓撲序列 31 for(register int j=0;j<edge[x].size();j++)//找鄰居 32 { 33 int y=edge[x][j]; 34 in[y]--;//度減1 35 if(!in[y])//找鄰居度為0的節點 36 q.push(y); 37 } 38 } 39 for(register int i=topo.size()-1;i>=0;i--) 40 { 41 int x=topo[i]; 42 f[x].reset();//置所以位為0 43 f[x][x]=1; // x這個點可以到達自己 f[x][x] =表示從 x出發的點, 44 for(register int k=0;k<edge[x].size();k++) 45 { 46 f[x]|=f[edge[x][k]];//x這個點可以到達的點的數量= {x} U {y1} U {y2}..{yn} 47 } 48 } 49 for(register int i=1;i<=n;i++) 50 cout<<f[i].count()<<endl;// f[i].count() 返回f[i] 中 1的個數 51 return 0; 52 }