Topological Sort-拓扑排序

楔子

在枚舉所有排列的問題之中,如果我們另外再限制誰要排在誰前方、誰要排在誰後方,那麼在這些限制之下,合理的排列還會剩下哪些呢?

【註:枚舉所有排列,讀者們可另行參考「 Enumerate all n-tuples 」一文。】

先後限制與圖

誰要排在誰前方、誰要排在誰後方,其實就是兩兩之間的關係,故可以改用圖來表示:把圖上一條由 A 點連向 B 點的邊,想成是 A 必須排在 B 前方( B 必須排在 A 後方)。

當然啦,也可以把圖上一條由 A 點連向 B 點的邊,想成是 A 必須排在 B 後方。不過一般來說我們習慣成自然地使用前者。

Topological Sort 與 Topological Ordering

「拓樸排序」是排序一張有向圖的點的方式。把圖上一條由 A 點連向 B 點的邊,想成是 A 必須排在 B 前方( B 必須排在 A 後方)。 Topological Sort 用來找出合理的排列順序,讓每一個點的先後順序,滿足每一條邊所規定的先後順序。

「拓樸順序」是指一張有向圖經過「拓樸排序」後,每一個點的先後順序。

一張圖經過 Topological Sort 的結果可以有很多種。只要不違背圖上每一條邊的先後規定,要怎麼排列圖上的點都行。

圖上不能有環

當圖上有環時,便無法進行 Topological Sort 。因為環上每一個點都會有連向自己的邊,意味著環上每一個點必須排在其他點的後方,環上每一個點都不能在排列順序中拔得頭籌,所以合理的排列順序不存在。

觀察問題

要找出合理的排列順序,首先得決定第一點!知道如何找出第一點,那麼就可以循序漸進的再找出第二點、第三點了。

可以作為第一點的點,想必它不必排在其他點後方。也就是說,沒有被任何邊連向的點,就可以作為第一點。如果有很多個第一點,那麼找哪一點都行。

決定第一點之後,那麼剩下所有點都會在第一點後方。也就是說,由第一點連出去的邊,其先後規定已經被滿足了,規定存不存在都無所謂。因此,決定第一點之後,就可以刪去此點,以及刪去由此點連出去的邊──原問題可以遞迴地縮小!

只要反覆的尋找沒有被任何邊連向的點,然後刪去此點以及刪去由此點連出去的邊,就可以找出一個合理的排列順序了。

附帶一提,要找出合理的排列順序,也可以由最後一點開始決定!無論要從第一點找到最後一點,或是從最後一點找到第一點,都是可以的。各位可以想想看該怎麼做。

找出一個合理的排列順序( adjacency matrix )

儘管這個問題有 Recursive 的性質,可以用遞迴實作,但由於遞迴的分支只有一條,故亦可以用迴圈實做。我想大家都會選擇以比較簡單的迴圈方式來實做吧?

實作時可以利用變數紀錄圖上每一個點目前仍被多少條邊連到。尋找沒有被任何邊連向的點,就直接看該變數是不是零;刪去由此點連出去的邊,就順便更新變數的值。

 
  1. bool adj[9][9]; // adjacency matrix
  2. int ref[9];     // 紀錄圖上每一個點目前仍被多少條邊連到
  3.  
  4. void topological_sort()
  5. {
  6.     for (int i=0i<9; ++iref[i] = 0// 初始化為0
  7.  
  8.     // 累計圖上每一個點被幾條邊連到
  9.     for (int i=0i<9; ++i)
  10.         for (int j=0j<9; ++j)
  11.             if (adj[i][j])
  12.                 ref[j]++;
  13.  
  14.     // 開始找出一個合理的排列順序
  15.     for (int i=0i<9; ++i)
  16.     {
  17.         // 尋找沒有被任何邊連向的點
  18.         int s = 0;
  19.         while (s < 9 && ref[s] != 0) ++s;
  20.  
  21.         if (s == 9break;  // 找不到。表示目前殘存的圖是個環。
  22.         ref[s] = -1;        // 設為已找過(刪去s點)
  23.  
  24.         cout << s;          // 印出合理的排列順序的第i點
  25.  
  26.         // 更新ref的值(刪去由s點連出去的邊)
  27.         for (int t=0t<9; ++t)
  28.             if (adj[s][t])
  29.                 ref[t]--;
  30.     }
  31. }

找出一個合理的排列順序( adjacency lists )

 
  1. int adj[9][9], size[9]; // adjacency lists
  2. int ref[9];     // 紀錄圖上每一個點目前仍被多少條邊連到
  3.  
  4. void topological_sort()
  5. {
  6.     for (int i=0i<9; ++iref[i] = 0// 初始化為0
  7.  
  8.     // 累計圖上每一個點被幾條邊連到
  9.     for (int i=0i<9; ++i)
  10.         for (int j=0j<size[i]; ++j)
  11.             ref[adj[i][j]]++;
  12.  
  13.     // 宣告一個queue來紀錄已經沒有被任何邊連向的點
  14.     queue<intQ;
  15.     for (int i=0i<9; ++i)
  16.         if (ref[i] == 0)
  17.             Q.push(i);
  18.  
  19.     // 開始找出一個合理的排列順序
  20.     for (int i=0i<9; ++i)
  21.     {
  22.         // 尋找沒有被任何邊連向的點
  23.         if (Q.empty()) break;       // 找不到。表示目前殘存的圖是個環。
  24.         int s = Q.front(); Q.pop();
  25.         ref[s] = -1;                // 設為已找過(刪去s點)
  26.  
  27.         cout << s;                  // 印出合理的排列順序的第i點
  28.  
  29.         // 更新ref的值(刪去由s點連出去的邊)
  30.         for (int j=0j<size[s]; ++j)
  31.         {
  32.             int t = adj[s][j];
  33.             ref[t]--;
  34.             if (!ref[t]) Q.push(t); // 紀錄已經沒有被任何邊連向的點
  35.         }
  36.     }
  37. }

時間複雜度

時間複雜度等於一次 Graph Traversal 的時間。圖的資料結構為 adjacency matrix 的話,便是 O(V^2) ;圖的資料結構為 adjacency lists 的話,便是 O(V+E) 。

UVa 10305 200

找出所有合理的排列順序

請用 backtracking 。此處不詳述了,直接看練習題吧。

UVa 124

計算所有合理的排列順序個數

需要使用 Dynamic Programming 解決,時間複雜度 O(2^V * V^2) 。

Topological Sort: 
Depth-first Search

程度★ 難度★★

Depth-first Search 與 Topological Sort 的關係

DFS 離開點的順序,顛倒之後,正好是拓樸順序。

DFS 優先走到最深的點,直到不能再深為止。 DFS 也會優先找出所有最深的點,離開點的原則是最深的點先離開。最深的點當然就是拓樸順序最後的點。

找出一個合理的排列順序( adjacency matrix )

 
  1. bool adj[9][9];     // adjacency matrix
  2. int visit[9];       // 記錄DFS遍歷過的點
  3. int order[9], n;    // 儲存一個合理的排列順序
  4.  
  5. bool cycle;         // 記錄DFS的過程中是否偵測到環
  6.  
  7. void DFS(int s)
  8. {
  9.     // back edge,有環。
  10.     if (visit[s] == 1cycle = true;
  11.     // forward edge、cross edge。
  12.     if (visit[s] == 2return;
  13.  
  14.     visit[s] = 1;
  15.     for (int t=0t<9; ++t)
  16.         if (adj[s][t])
  17.             DFS(t);
  18.     visit[s] = 2;
  19.  
  20.     order[n--] = s;     // 記錄合理的排列順序
  21. }
  22.  
  23. void topological_sort()
  24. {
  25.     // 初始化
  26.     for (int i=0i<9i++) visit[i] = 0;
  27.     cycle = false;
  28.     n = 9-1;
  29.  
  30.     // 進行DFS
  31.     for (int s=0s<9; ++s)
  32.         if (!v[s])
  33.             DFS(s);
  34.  
  35.     // 輸出結果
  36.     if (cycle)
  37.         cout << "圖上有環";
  38.     else
  39.         // 印出一個合理的排列順序
  40.         for (int i=0i<9; ++i)
  41.             cout << order[i];
  42. }


你可能感兴趣的:(Topological Sort-拓扑排序)