拓撲排序(提升)
什麼是拓撲排序?
維基百科對於拓撲排序有如下定義:
a topological sort or topological ordering of a directed graph is a linear ordering of
its vertices such that for every directed edge uv from vertex u to vertex v, u comes
before v in the ordering.
即:對於任何有向圖而言,其拓撲排序為其所有結點的一個線性排序(對於同一個有向圖而言可能存在多個這樣的結點排序)。該排序滿足這樣的條件——對於圖中的任意兩個結點 u
v
,若存在一條有向邊從 u
指向 v
,則在拓撲排序中 u
一定出現在 v
前面。
拓撲排序主要用來解決有向圖中的依賴解析(dependency resolution)問題。
舉例來說,如果我們將一系列需要執行的任務構成一個有向圖,圖中的有向邊則代表某一任務必須在另一個任務之前完成這一限制。那麼運用拓撲排序,我們就能得到滿足執行順序限制條件的一系列任務所需執行的先後順序。當然也有可能圖中並不存在這樣一個拓撲順序,這種情況下我們無法根據給定要求完成這一系列任務,這種情況稱為迴圈依賴(circular dependency)。
拓撲排序存在的前提
當且僅當一個有向圖為有向無環圖(directed acyclic graph,或稱DAG)時,才能得到對應於該圖的拓撲排序。每一個有向無環圖都至少存在一種拓撲排序。該論斷可以利用反證法被證明如下:
假設我們有一由 v_1
到 v_n
這 n 個結點構成的有向圖,且圖中 v_1,v_2,...,v_n
這些結點構成一個環。這即是說對於所有 1≤i<n-1
,圖中存在一條有向邊從 v_i
指向 v_i+1
。同時還存在一條從 v_n
指向 v_1
的邊。假設該圖存在一個拓撲排序。
那麼基於這樣一個有向圖,顯然我們可以得知對於所有 1≤i<n-1,v_i
必須在 v_i+1
之前被遍歷,也就是 v_1
必須在 v_n
之前被遍歷。同時由於還存在一條從 v_n
指向 v_1
的邊,v_n
必須在 v_1
之前被遍歷。這裡出現了與我們的假設所衝突的結果。因此我們可以知道,該圖存在拓撲排序的假設不成立。也就是說,對於非有向無環圖而言,其拓撲排序不存在。
拓撲排序的演算法和實現
拓撲排序的問題存在一個線性時間解。也就是說,若有向圖中存在 n 個結點,則我們可以在 \(O(n)\) 時間內得到其拓撲排序,或在 \(O(n)\) 時間內確定該圖不是有向無環圖,也就是說對應的拓撲排序不存在。
例如一個有向無環圖如下:
根據圖中的邊的方向,我們可以看出,若要滿足得到其拓撲排序,則結點被遍歷的順序必須滿足如下要求:
1.結點1必須在結點2、3之前
2.結點2必須在結點3、4之前
3.結點3必須在結點4、5之前
4.結點4必須在結點5之前
則一個滿足條件的拓撲排序為 [1, 2, 3, 4, 5]
。
若我們刪去圖中4、5結點之前的有向邊,上圖變為如下所示:
則我們可得到兩個不同的拓撲排序結果:[1, 2, 3, 4, 5]
和 [1, 2, 3, 5, 4]
。
為了說明如何得到一個有向無環圖的拓撲排序,我們首先需要了解有向圖結點的入度(indegree)和出度(outdegree)的概念。
假設有向圖中不存在起點和終點為同一結點的有向邊。
入度:設有向圖中有一結點 v
,其入度即為當前所有從其他結點出發,終點為 v
的的邊的數目。也就是所有指向 v
的有向邊的數目。
出度:設有向圖中有一結點 v
,其出度即為當前所有起點為 v
,指向其他結點的邊的數目。也就是所有由 v
發出的邊的數目。
在瞭解了入度和出度的概念之後,再根據拓撲排序的定義,我們自然就能夠得出結論:要想完成拓撲排序,我們每次都應當從入度為 0 的結點開始遍歷。因為只有入度為 0 的結點才能夠成為拓撲排序的起點。否則根據拓撲排序的定義,只要一個結點 v
的入度不為 0,則至少有一條邊起始於其他結點而指向 v
,那麼這條邊的起點在拓撲排序的順序中應當位於 v
之前,則 v
不能成為當前遍歷的起點。
由此我們可以進一步得出一個改進的深度優先遍歷或廣度優先遍歷演算法來完成拓撲排序。以廣度優先遍歷為例,這一改進後的演算法與普通的廣度優先遍歷唯一的區別在於我們應當儲存每一個結點對應的入度,並在遍歷的每一層選取入度為0的結點開始遍歷(而普通的廣度優先遍歷則無此限制,可以從該吃呢個任意一個結點開始遍歷)。這個演算法描述如下:
1.初始化一個int[] inDegree儲存每一個結點的入度。
2.對於圖中的每一個結點的子結點,將其子結點的入度加1。
3.選取入度為0的結點開始遍歷,並將該節點加入輸出。
4.對於遍歷過的每個結點,更新其子結點的入度:將子結點的入度減1。
5.重複步驟3,直到遍歷完所有的結點。
6.如果無法遍歷完所有的結點,則意味著當前的圖不是有向無環圖。不存在拓撲排序。
板子
bool topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
return tt == n - 1;
}
例題
有個人的家族很大,輩分關係很混亂,請你幫整理一下這種關係。
給出每個人的孩子的資訊。
輸出一個序列,使得每個人的孩子都比那個人後列出。
輸入格式
第 1 行一個整數 n,表示家族的人數;
接下來 n 行,第 i 行描述第 i 個人的孩子;
每行最後是 0 表示描述完畢。
每個人的編號從 1 到 n。
輸出格式
輸出一個序列,使得每個人的孩子都比那個人後列出;
資料保證一定有解,如果有多解輸出任意一解。
資料範圍
1≤n≤100
輸入樣例:
5
0
4 5 1 0
1 0
5 3 0
3 0
輸出樣例:
2 4 5 3 1
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110, M = N * N / 2;
int n;
int h[N], e[M], ne[M], idx;
int q[N];
int d[N];
void add (int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i ++ )
{
int son;
while (cin >> son, son)
{
add(i, son);
d[son] ++ ;
}
}
topsort();
for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
return 0;
}
由於無敵的凡凡在2005年世界英俊帥氣男總決選中勝出,Yali Company總經理Mr.Z心情好,決定給每位員工發獎金。
公司決定以每個人本年在公司的貢獻為標準來計算他們得到獎金的多少。
於是Mr.Z下令召開 m 方會談。
每位參加會談的代表提出了自己的意見:“我認為員工 a 的獎金應該比 b 高!”
Mr.Z決定要找出一種獎金方案,滿足各位代表的意見,且同時使得總獎金數最少。
每位員工獎金最少為100元,且必須是整數。
輸入格式
第一行包含整數 n,m,分別表示公司內員工數以及參會代表數。
接下來 m 行,每行 2 個整數 a,b,表示某個代表認為第 a 號員工獎金應該比第 b 號員工高。
輸出格式
若無法找到合理方案,則輸出“Poor Xed”;
否則輸出一個數表示最少總獎金。
資料範圍
1≤n≤10000,
1≤m≤20000
輸入樣例:
2 1
1 2
輸出樣例:
201
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 20010;
int n, m;
int h[N], e[M], ne[M], idx;
int q[N];
int d[N];
int dist[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
bool topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
return tt == n - 1;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(b, a);
d[a] ++ ;
}
if (!topsort()) puts("Poor Xed");
else
{
for (int i = 1; i <= n; i ++ ) dist[i] = 100;
for (int i = 0; i < n; i ++ )
{
int j = q[i];
for (int k = h[j]; ~k; k = ne[k])
dist[e[k]] = max(dist[e[k]], dist[j] + 1);
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res += dist[i];
printf("%d\n", res);
}
return 0;
}
給定一張 N 個點 M 條邊的有向無環圖,分別統計從每個點出發能夠到達的點的數量。
輸入格式
第一行兩個整數 N,M,接下來 M 行每行兩個整數 x,y,表示從 x 到 y 的一條有向邊。
輸出格式
輸出共 N 行,表示每個點能夠到達的點的數量。
資料範圍
1≤N,M≤30000
輸入樣例:
10 10
3 8
2 3
2 5
5 9
5 9
2 3
3 9
4 8
2 10
4 9
輸出樣例:
1
6
3
3
2
1
1
1
1
1
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <bitset>
using namespace std;
const int N = 30010, M = 30010;
int n, m;
int h[N], e[M], ne[M], idx;
int d[N], q[N];
bitset<N> f[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
d[b] ++ ;
}
topsort();
for (int i = n - 1; i >= 0; i -- )
{
int j = q[i];
f[j][j] = 1;
for (int k = h[j]; ~k; k = ne[k])
f[j] |= f[e[k]];
}
for (int i = 1; i <= n; i ++ ) printf("%d\n", f[i].count());
return 0;
}
超經典例題車站分級
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2010, M = 1000010;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
int q[N], d[N];
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
d[b] ++ ;
}
void topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n + m; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 1; i <= m; i ++ )
{
memset(st, 0, sizeof st);
int cnt;
scanf("%d", &cnt);
int start = n, end = 1;
while (cnt -- )
{
int stop;
scanf("%d", &stop);
start = min(start, stop);
end = max(end, stop);
st[stop] = true;
}
int ver = n + i;
for (int j = start; j <= end; j ++ )
if (!st[j]) add(j, ver, 0);
else add(ver, j, 1);
}
topsort();
for (int i = 1; i <= n; i ++ ) dist[i] = 1;
for (int i = 0; i < n + m; i ++ )
{
int j = q[i];
for (int k = h[j]; ~k; k = ne[k])
dist[e[k]] = max(dist[e[k]], dist[j] + w[k]);
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, dist[i]);
printf("%d\n", res);
return 0;
}