圖論篇4——拓撲排序


引入

有向無環圖(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;
}
View Code

反向拓撲排序

題目鏈接: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;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM