拓撲排序(topsort)算法詳解

  在圖論中,由某個集合上的偏序獲得全序的策略就是拓補排序算法。拓撲排序常出如今涉及偏序關係的問題中,例如時序的前後、事物的依賴等。針對這些問題拓撲排序一般能有效地給出可行解。ios

  爲了便於理解,咱們先來看一個實例,開源軟件常使用GNU make工具來管理項目的構建,這裏的「項目」是由若干個「對象」構成的。Makefile文件則描述了這些「對象」的構建規則,即給出一系列對象間的依賴關係。若對象A依賴於對象B,則說明對象B必須先於對象A構建,不然構建將沒法進行。make的任務就是合理安排各個對象構建的前後順序,使得過程能順利地完成。算法

  做爲例子,一個Makefile文件的內容以下:每行描述一個規則。例如第一行指明對象foo.o和bar.o必須先於target構建。數組

target: foo.o bar.o

foo.o: foo.c foo.h
bar.o: bar.c bar.h

  咱們先對問題進行數學轉化。離散數學爲咱們描述對象間的關係提供了有力工具——偏序。令X爲全部要研究的對象的集合。集合X上的一個關係R是偏序,當且僅當R知足自反性、對稱性、傳遞性。工具

  定義以下關係:xRy:x必須先於y被構建,即y依賴x,由於R知足偏序的性質,xRy也記爲x≼y。到此咱們成功地對問題進行了建模。接下來使用DAG來表示每一個對象間的關係,圖的每個頂點表示一個對象、有向線段表示關係 起點終點佈局

              (圖一)spa

  如何合理佈局各個對象的構建順序,使得構建過程能夠順利地進行下去呢?假設咱們根本不知道拓撲排序,直觀的想法是:先選擇不被其它對象依賴的做爲第1個對象;再考慮第2個對象,它除了已選的第1個對象外,不該該被其它對象依賴;選擇第n個對象,它除了前面已選的第1~n-1對象外,不能再被其它對象依賴。按照這個規則依次選出對象,便可保證構建過程順利結束。code

  能夠證實這種直觀想法是正確的。這種策略的結果用下圖描述,可是,這並非真正的拓撲排序:對象

              (圖二)blog

  問題在於,獲得的圖並無反映排序後各對象間的關係,充其量不過把圖從新擺了一個形態,而圖所描述的關係並無本質的改變。如何解決這一問題?這就要引入全序。從圖中直觀地看出,只有部分對象之間具備偏序關係,做爲反例,bar.h與bar.c之間無偏序關係,所以R不是集合X上的全序關係。試想在圖中,若是爲每一對不能比較的對象<u,v>,強制添加一個關係u≼v,使得集合X中每兩個對象都能創建關係,則R就成爲了X上的全序關係,如圖三所示。排序

              (圖三)

  按照Hass圖的順序排列各個頂點獲得圖三,咱們發現從最底部頂點bar.c出發,總有一條路徑能走完全部頂點併到達最頂部頂點target,所以咱們獲得的結果拓撲有序線性排列全部頂點,以下圖所示:

  這個結果即是原問題的拓撲排序。由於添加關係u≼v的方法不惟一,因此拓撲排序不是惟一的。但不管哪一種狀況,拓撲排序都知足一個關鍵的性質:沒有一個節點指向它前面的節點,形式化地描述:對於圖中的任意兩個結點u和v,若存在一條有向邊<u,v>,則在拓撲排序中u必定出如今v前面。這條性質描述了拓撲排序的本質,爲咱們編寫可行的算法提供了依據。

  另一些須要知道的定理是,有向圖拓撲排序存在的充分必要條件是圖爲DAG(有向無環圖),這個結論用於判斷問題是否有解,也可用於判斷一個有向圖是否有環。

 

  算法的求解過程以下:首先統計全部頂點的入度。而後:

a. 尋找全部入度爲0的頂點,追加到結果序列末尾並將其從圖中移除,同時將其全部鄰接頂點的入度減一。
b. 重複a,直到全部頂點都從圖中移除。

  算法結束時,所得結果序列即是最終答案。
  對於任意一個可能帶環的有向圖,在尋找入度爲0的頂點時,若是找不到,說明圖的拓撲排序是不存在的,即問題無解。

 

  上述的「移除」是邏輯層面的概念,具體實現中,咱們不須要真正地將頂點從圖中移除,由於某次a.中找到的入度爲0的頂點只可能出如今上一次a.中入度被減一的頂點中。當a找到入度爲0的頂點時,就會把它的鄰接頂點的入度減一,這時即可以順便統計入度減爲0的頂點,下次a直接從這些入度爲0的頂點開始,無需再從整個圖中尋找入度爲0的頂點。

 

  最後經過一道UVa的題目來講明算法的具體實現:

UVa10305(Ordering Tasks)

題目大意

  給出一堆任務,其中一個任務必須在它依賴的全部任務都完成後才能執行。已知任務之間的關係,求可能的執行順序。

分析

  思路與make的例子一致。這裏使用vector存儲鄰接表,數組deg_in維護每一個頂點的入度,隊列que維護每趟中入度被減爲0的頂點。

參考代碼

#include <iostream>
#include <queue>
#define N 100+2

using namespace std;

static vector<int> con[N];
static int deg_in[N];

int main(void) {
    ios::sync_with_stdio(false);
    
    int n,m;
    while((cin >> n >> m) && n) {
        for(int i=1;i<=n;++i) {
            con[i].clear();
            deg_in[i] = 0;
        }
        
        for(int i=0; i<m; ++i) {
            int a,j;
            cin >> a >> j;
            con[a].push_back(j);
            ++deg_in[j];
        }
        
        //
        // 找出第一個度爲0的頂點
        //
        queue<int> que;
        vector<int> ans;
        for(int i=1; i<=n; ++i) {
            if (!deg_in[i]) {
                que.push(i);
            }
        }
        
        //
        // 求排序中其它n-1個頂點
        //
        while(!que.empty()) {
            int u = que.front();
            que.pop();
            
            ans.push_back(u);
            
            for(size_t i=0; i<con[u].size(); ++i) {
                int t = con[u][i];
                if (--deg_in[t] == 0) {
                    que.push(t);
                }
            }
        }
        
        for(size_t i=0; i<ans.size(); ++i) {
            cout << ans[i] << (i==ans.size()-1 ? "" : " ");
        }
        cout << endl;
    }
    
    return 0;
}
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息