拓扑排序基础讲解
拓撲排序(TopSort)
2021年8月5日
文章目錄
- 拓撲排序(TopSort)
- 1、算法原理
- 1.1 一個問題
- 1.2 如何通過計算機輸出排序結果?
- 1.3 圖示
- 1.4 不穩定性
- 1.5 無法排序?
- 1.6 時間復雜度
- 2、算法實現
- 2.1 題目
- 2.2 解法分析
- 2.3 完整代碼
- 幾道例題
- A:[POJ - 2367 ](https://vjudge.net/problem/POJ-2367/origin) Genealogical tree
- B:[HDU - 1285](https://vjudge.net/problem/HDU-1285/origin)[ 確定比賽名次](https://vjudge.net/problem/HDU-1285)
- C:[HDU - 2094](https://vjudge.net/problem/HDU-2094/origin)[產生冠軍](https://vjudge.net/problem/HDU-2094)
1、算法原理
1.1 一個問題
在日常生活中,我們的某些行為需要有先后順序。比如,你需要先穿襪子再穿鞋,先拿起手表再戴手表,順序無法改變。
同理在一個工程中,每一步的工作都需要先后順序,某些工作的完成是另一些工作的前置條件。我們稱這每一步的工作為事件。
那么,若已知每個事件的前置事件,怎么確定其完成的先后順序?
我們可以構建一個網絡,將每個事件的前置事件放置在這個事件之前,并用有向線段連接,由此可以直觀地看出我們需要完成事件的先后順序。
此時構建的網絡,通常被稱為頂點活動網(Activity On Vertex network),簡稱AOV網。
1.2 如何通過計算機輸出排序結果?
拓撲排序就是針對這一類問題進行的排序算法。
建立上述AOV網之后,我們可對每個事件所構成的結點進行入度的記錄。
首先,將所有入度為0的結點加入隊列(這些事件不需要前置事件),對其后續結點進行遍歷。遍歷到一個結點后,此結點的入度就減1,當入度減為0,加入隊列(前置事件均已完成),對其后續結點進行遍歷。
當所有結點遍歷過后,就可以知道其先后順序了。
1.3 圖示
首先,將入度為0的1,2,4結點加入隊列,首先遍歷1后的結點:
此時,結點3的入度減去1,變為1,不等于0,先不進行操作,隨后,對2后的結點進行遍歷:
此時,結點3的入度減為0,加入隊列。
接著,如下操作:
所有結點遍歷完畢,其先后順序即為 1 2 4 3 5 6。
1.4 不穩定性
拓撲排序無其他排序條件時是不穩定的,與你加入隊列的順序與方式有關,比如上面圖示樣例中也可寫成1 4 2 3 5 6,4 1 2 3 5 6等等。
因為其在實際問題中,同層的先后順序是無關事件,因此不影響其正確性,若對排序有另外的要求,則按照要求的方式加入隊列即可。
1.5 無法排序?
當圖是有環圖,意味著一定有結點無法將其入度減為0,因此無法進行其后續結點的排序,如圖:
此時不存在入度為0的點,因此無法得知其先后順序。
因此,我們需要在進行排序時,判斷所有遍歷過的點數目是否等于所有點的數目,若不等于,則表示其存在環,排序失敗,若等于,則排序成功。
1.6 時間復雜度
因每個結點只需加入隊列一次,而每個結點加入隊列前需要進行入度數量次的操作,因此時間復雜度為O(n+m)。
2、算法實現
2.1 題目
給你n個有向邊和m個點,若能進行拓撲排序,輸出排序后的序列,若不能,輸出-1.
輸出為1行,答案可能不唯一,輸出正解中任意一種。
2.2 解法分析
首先,記錄所有點的入度,此時設置數組(或其他記錄方式)inp[],在每次輸入時,對后一個事件的入度進行加1,并記錄前一個事件的后續事件:
for(int i = 0; i < n; i++){cin>>a>>b;inp[b]++;v[a].push_back(b); }隨后,將每個入度為0的結點加入隊列:
queue<int>q; for (int i = 1; i <= m; i++) {//注意點的標號從幾開始if (!inp[i])q.emplace(i);//為0則加入隊列 }接著,對圖按照BFS進行遍歷:
while (!q.empty()) {int fa = q.front();q.pop();pd++;//點數量的記錄ans[num++] = fa;//記錄順序for (auto now : v[fa]) {inp[now]--;//入度減一if (!inp[now]) {//為0則加入隊列q.emplace(now);}} }最后對是否排序成功進行判斷:
if(pd==m)return 1; else return 0;2.3 完整代碼
#include<iostream> #include<algorithm> #include<stdio.h> #include<vector> #include<queue>using namespace std;int inp[1005]; int n, m, pd; vector<int>v[1005]; int ans[1005];bool bfs() {queue<int>q;for (int i = 1; i <= m; i++) {if (!inp[i])q.emplace(i);//為0則加入隊列}while (!q.empty()) {int fa = q.front();q.pop();ans[pd++] = fa;for (auto now : v[fa]) {inp[now]--;//入度減一if (!inp[now]) {//為0則加入隊列q.emplace(now);}}}if (pd == m)return true;else return false; }int main() {int a, b;cin >> n >> m;for (int i = 0; i < n; i++) {cin >> a >> b;inp[b]++;v[a].push_back(b);}if (bfs()) {cout << ans[0];for (int i = 1; i < m; i++){cout << ' ' << ans[i];}}else {cout << -1;}cout << endl;return 0; }樣例輸入1:
5 6 1 3 2 3 3 5 4 5 5 6預期結果1:
1 2 4 3 5 6測試結果1:
樣例輸入2:
6 6 1 2 2 3 3 4 4 5 5 2 4 6預期結果2:
-1測試結果2:
以上樣例均通過。
幾道例題
A:POJ - 2367 Genealogical tree
Description
The system of Martians’ blood relations is confusing enough. Actually, Martians bud when they want and where they want. They gather together in different groups, so that a Martian can have one parent as well as ten. Nobody will be surprised by a hundred of children. Martians have got used to this and their style of life seems to them natural.
And in the Planetary Council the confusing genealogical system leads to some embarrassment. There meet the worthiest of Martians, and therefore in order to offend nobody in all of the discussions it is used first to give the floor to the old Martians, than to the younger ones and only than to the most young childless assessors. However, the maintenance of this order really is not a trivial task. Not always Martian knows all of his parents (and there’s nothing to tell about his grandparents!). But if by a mistake first speak a grandson and only than his young appearing great-grandfather, this is a real scandal.
Your task is to write a program, which would define once and for all, an order that would guarantee that every member of the Council takes the floor earlier than each of his descendants.
Input
The first line of the standard input contains an only number N, 1 <= N <= 100 — a number of members of the Martian Planetary Council. According to the centuries-old tradition members of the Council are enumerated with the natural numbers from 1 up to N. Further, there are exactly N lines, moreover, the I-th line contains a list of I-th member’s children. The list of children is a sequence of serial numbers of children in a arbitrary order separated by spaces. The list of children may be empty. The list (even if it is empty) ends with 0.
Output
The standard output should contain in its only line a sequence of speakers’ numbers, separated by spaces. If several sequences satisfy the conditions of the problem, you are to write to the standard output any of them. At least one such sequence always exists.
Sample Input
5
0
4 5 1 0
1 0
5 3 0
3 0
Sample Output
2 4 5 3 1
分析
此題輸入第i個結點的后續結點,直接建圖拓撲排序即可。
代碼
#include<iostream> #include<algorithm> #include<stdio.h> #include<vector> #include<queue>using namespace std;int inp[1005], n, pd, num; vector<int>v[1005]; int ans[1005];bool bfs() {queue<int>q;for (int i = 1; i <= n; i++) {if (!inp[i])q.emplace(i);//為0則加入隊列}while (!q.empty()) {int fa = q.front();q.pop();ans[pd++] = fa;for(int i = 0; i < v[fa].size(); i++){int now = v[fa][i];inp[now]--;//入度減一if (!inp[now]) {//為0則加入隊列q.emplace(now);}}}if (pd == n)return true;else return false; }int main() {int a;cin >> n;for (int i = 0; i < n; i++) {while (cin >> a, a != 0) {v[i + 1].push_back(a);inp[a]++;}}if (bfs()) {cout << ans[0];for (int i = 1; i < num; i++) {cout << ' ' << ans[i];}}else {cout << -1;}cout << endl;return 0; }B:HDU - 1285 確定比賽名次
Problem Description
有N個比賽隊(1<=N<=500),編號依次為1,2,3,。。。。,N進行比賽,比賽結束后,裁判委員會要將所有參賽隊伍從前往后依次排名,但現在裁判委員會不能直接獲得每個隊的比賽成績,只知道每場比賽的結果,即P1贏P2,用P1,P2表示,排名時P1在P2之前。現在請你編程序確定排名。
Input
輸入有若干組,每組中的第一行為二個數N(1<=N<=500),M;其中N表示隊伍的個數,M表示接著有M行的輸入數據。接下來的M行數據中,每行也有兩個整數P1,P2表示即P1隊贏了P2隊。
Output
給出一個符合要求的排名。輸出時隊伍號之間有空格,最后一名后面沒有空格。
其他說明:符合條件的排名可能不是唯一的,此時要求輸出時編號小的隊伍在前;輸入數據保證是正確的,即輸入數據確保一定能有一個符合要求的排名。
Sample Input
4 3
1 2
2 3
4 3
Sample Output
1 2 4 3
分析:
此題要求隊伍編號小的隊伍在前,因此需要使用優先隊列,然后建圖拓撲排序即可。
代碼:
#include<iostream> #include<algorithm> #include<stdio.h> #include<vector> #include<queue> #include<functional>using namespace std;int inp[505], n, m; vector<int>v[505]; int ans[505];void bfs() {//queue<int>q;int pd = 0;priority_queue<int, vector<int>, greater<int>>q;//因題目要求,此處采用優先隊列for (int i = 1; i <= n; i++) {if (!inp[i])q.emplace(i);//為0則加入隊列}while (!q.empty()) {int fa = q.top();q.pop();ans[pd++] = fa;for(int i = 0; i < v[fa].size(); i++){int now = v[fa][i];inp[now]--;//入度減一if (!inp[now]) {//為0則加入隊列q.emplace(now);}}} }int main() {int a, b;while (cin>>n>>m) {memset(inp, 0, sizeof inp);memset(v, 0, sizeof v);for (int i = 0; i < m; i++) {cin >> a >> b;inp[b]++;v[a].push_back(b);}bfs();cout << ans[0];for (int i = 1; i < n; i++) {cout << ' ' << ans[i];}cout << '\n';}return 0; }C:HDU - 2094產生冠軍
Problem Description
有一群人,打乒乓球比賽,兩兩捉對撕殺,每兩個人之間最多打一場比賽。
球賽的規則如下:
如果A打敗了B,B又打敗了C,而A與C之間沒有進行過比賽,那么就認定,A一定能打敗C。
如果A打敗了B,B又打敗了C,而且,C又打敗了A,那么A、B、C三者都不可能成為冠軍。
根據這個規則,無需循環較量,或許就能確定冠軍。你的任務就是面對一群比賽選手,在經過了若干場撕殺之后,確定是否已經實際上產生了冠軍。
Input
輸入含有一些選手群,每群選手都以一個整數n(n<1000)開頭,后跟n對選手的比賽結果,比賽結果以一對選手名字(中間隔一空格)表示,前者戰勝后者。如果n為0,則表示輸入結束。
Output
對于每個選手群,若你判斷出產生了冠軍,則在一行中輸出“Yes”,否則在一行中輸出“No”。
Sample Input
3
Alice Bob
Smith John
Alice Smith
5
a c
c d
d e
b e
a d
0
Sample Output
Yes
No
分析:
此題是考察拓撲排序的性質,即若冠軍只有一人的話,則初始化圖的入度為0的僅有一個結點,因此直接枚舉入度為0結點數量即可。
代碼:
#include<iostream> #include<algorithm> #include<stdio.h> #include<string> #include<map>using namespace std;map<string, int>mp;bool solve(int n) {int poc = 0;for (auto x : mp) {if (!x.second) {poc++;}}if (poc == 1)return 1;elsereturn 0; } int main() {int n;while (cin >> n, n != 0) {mp.clear();string k, l;for (int i = 0; i < n; i++) {cin >> k >> l;mp[k] = mp[k];mp[l]++;}if (solve(n))cout << "Yes\n";elsecout << "No\n";}return 0; }總結
- 上一篇: 内存插13跟24一样吗
- 下一篇: 2021-08-05学习日记