引入
有向無環圖(DAG)
如果一個有向圖不存在環,也就是任意結點都無法通過一些有向邊回到自身,那么稱這個有向圖為有向無環圖。
AOV網絡
在有向圖中,用頂點表示活動,用有向邊$ < V_i, V_j > $表示活動 $i$ 是活動 $j$ 的必須條件。這種有向圖稱為用頂點表示活動的網絡(Active on vertices),簡稱AOV網絡。
在AOV網絡中,如果活動$V_i$必須在$V_j$之前進行,則存在有向邊$<V_i, V_j>$,並稱$V_i$是$V_j$的直接前驅,$V_j$是$V_i$的直接后繼。這種前驅與后繼的關系具有傳遞性和反自反性,這要求AOV網絡中不能出現回路,即有向環。因此,對於給定的AOV網絡,必須先判斷它是否存在有向環。
拓撲排序
檢測有向環可以通過對AOV網絡進行拓撲排序,該過程將各個頂點排列成一個線性有序的序列,使得AOV網絡中所有的前驅和后繼關系都能得到滿足。 如果拓撲排序能夠將AOV網絡的所有頂點都排入一個拓撲有序的序列中,則說明該AOV網絡中沒有有向環,否則AOV網絡中必然存在有向環。AOV網絡的頂點的拓撲有序序列不唯一。可以將拓撲排序看做是將圖中的所有節點在一條水平線上的展開,圖的所有邊都從左指向右。
用計算機專業的幾門課程的學習次序來描述拓撲關系(打個比方,圖內容可能不是特別嚴謹) ,顯然對於一門課來說,必須先學習它的先導課程才能更好地學習這門課程,比如學數據結構必須先學習C語言和離散數學,而且先導課程中不能有環,否則沒有盡頭了(多瑪姆,我是來談條件的?)
而且還可以發現,如果兩門課程之間沒有直接或間接的先導關系,那么這兩門課的學習先后是任意的(比如“C語言”和“離散數學”的學習順序就是任意的),於是上述課程就可以排成一個水平展開的先后順序,如下圖
拓撲排序的結果不唯一,比如“C語言”和“離散數學”就可以換下順序,又或者把“計算機導論”向前放在任何一個位置都可以。總結一下就是,如果某一門課沒有先導課程或是所有的先導課程都已經學習完畢,那么這門課就可以學習了。如果同時有多門這樣的課,它們的學習順序任意。
算法描述
對於一個有向無環圖
(1)統計所有節點的入度,對於入度為0的節點就可以分離出來,然后把這個節點指向所有的節點的入度$-1$。
(2)重復(1),直到所有的節點都被分離出來,拓撲排序結束。
(3)如果最后不存在入度為0的節點,那就說明有環,無解。
解釋一下,假設A為一個入度為0的結點,就表示A結點沒有前驅結點,可以直接做,把A完成后,對於A的所有后繼結點來說,前驅結點就完成了一個,入度進行$-1$。
時間復雜度
如果AOV網絡有n個頂點,e條邊,在拓撲排序的過程中,搜索入度為零的頂點所需的時間是O(n)。在正常情況下,每個頂點進一次棧,出一次棧,所需時間O(n)。每個頂點入度減1的運算共執行了e次。所以總的時間復雜為O(n+e)。
因為拓撲排序的結果不唯一,所以題目一般會要求按某種順序輸出,就需要使用優先級隊列,這里采取了最小字典序輸出。
vector<int>head[505], ans; int n, m, in[505];//入度序列 void topologicalSorting() { cin >> n >> m; for (int i = 0; i < m; i++) { int c1, c2; scanf("%d%d", &c1, &c2); head[c1].push_back(c2); in[c2]++; } priority_queue<int, vector<int>, greater<int>>q; for (int i = 1; i <= n; i++) { if (!in[i]) { q.push(i); } } while (!q.empty() && ans.size() < n) { int v = q.top(); q.pop(); ans.push_back(v); for (int i = 0; i < head[v].size(); i++) { in[head[v][i]]--; if (!in[head[v][i]]) q.push(head[v][i]); } } if (ans.size() == n) { //找到拓撲排序序列 } else { //圖中有環 } }
練習
模板題
題目鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=1285

#include <iostream> #include <algorithm> #include <queue> #include <stdio.h> #include <vector> using namespace std; vector<int>head[505]; int in[505]; int main() { int n, m; while (cin >> n >> m) { priority_queue<int, vector<int>, greater<int>>q; vector<int>ans; for (int i = 0; i < m; i++) { int c1, c2; scanf("%d%d", &c1, &c2); head[c1].push_back(c2); in[c2]++; } for (int i = 1; i <= n; i++) { if (!in[i]) { q.push(i); } } while (!q.empty()) { int temp = q.top(); q.pop(); ans.push_back(temp); for (int i = 0; i < head[temp].size(); i++) { in[head[temp][i]]--; if (!in[head[temp][i]]) q.push(head[temp][i]); } } if (ans.size() == n) { for (int i = 0; i < n; i++) { head[i + 1].clear(); cout << ans[i]; if (i != n - 1)cout << ' '; } cout << endl; } q.emplace(); ans.clear(); } return 0; }
反向拓撲排序
題目鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=4857
題意:$n$個結點,給定$m$個拓撲關系$(a,b)$表示$a$必須排在$b$前面,在滿足$m$個拓撲關系關系的同時,使得小的結點盡可能的排在前面。
乍一看好像直接拓撲排序就行,但是看一個例子:
$6 \rightarrow 3\rightarrow 1\\5 \rightarrow 4 \rightarrow 2$
直接拓撲排序的結果是:$5\ 4\ 2\ 6\ 3\ 1$ ,是錯誤的,因為我們可以把1號安排到更前面的位置 即:$6\ 3\ 1\ 5\ 4\ 2$(正確答案)。所以直接拓撲排序是不行的,為什么會出現這樣的狀況,對於多個拓撲關系,我們本來的策略是優先刪除首結點較小的拓撲序列(比如5號結點比6號結點小,我們先刪除了5號結點),但我們希望的是優先刪除尾結點較小的拓撲序列(比如1號結點比2號結點小,應當先刪除1號結點)。問題找到了,我們可以嘗試一下逆向思維,即我們先考慮哪些點應該靠后釋放,就是把原來的拓撲關系反過來
$1 \rightarrow 3 \rightarrow 6\\2 \rightarrow 4 \rightarrow 5$
這樣我們按照優先刪除首結點較大的拓撲序列得到的結果是$2\ 4\ 5\ 1\ 3\ 6$,好像還是不太對,把它逆序輸出就對啦!
#include <iostream> #include <algorithm> #include <queue> #include <stdio.h> #include <vector> using namespace std; vector<int>head[30005]; int in[30005]; int main() { int T; cin >> T; while (T--) { int n, m; cin >> n >> m; priority_queue<int>q; vector<int>ans; for (int i = 0; i < m; i++) { int c1, c2; scanf("%d%d", &c1, &c2); /*head[c1].push_back(c2); in[c2]++;*/ head[c2].push_back(c1); in[c1]++; } for (int i = 1; i <= n; i++) { if (!in[i]) { q.push(i); } } while (!q.empty()) { int temp = q.top(); q.pop(); ans.push_back(temp); for (int i = 0; i < head[temp].size(); i++) { in[head[temp][i]]--; if (!in[head[temp][i]]) q.push(head[temp][i]); } } if (ans.size() == n) { for (int i = n - 1; i >= 0 ; i--) { head[i + 1].clear(); cout << ans[i]; if (i != 0)cout << ' '; } cout << endl; } q.emplace(); ans.clear(); } return 0; }