拓撲排序小結

江上舟搖發表於2022-04-29

首先來理解什麼是拓撲排序;拓撲排序簡單說是做事情的先後順序,在現實生活中,人們經常要做連串事情,這些事情之聞有順序關係或者依賴關係,在做一件事情之前必須先做另一件事,比如安排客人的座位、穿衣服的先後、課程學習的先後等。這些事情都可以抽象為圖論中的拓撲排序。

拓撲排序的概念:
設有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 }

 

相關文章