概念
拓撲排序
拓撲排序是一種在DAG上進行的算法。拓撲排序可以解決有若干對關系,需要求出滿足所有關系的一種方案/最大值/最小值的問題。拓撲排序可以求出圖中的一條拓撲序列。
拓撲排序可以檢查出有向圖中是否存在環,如果一個有向圖存在合法的拓撲序列,說明該有向圖內無環;反之,說明該有向圖中有環。拓撲排序是無法給有向帶環圖求出拓撲序列的。
拓撲排序的時間復雜度是\(O(N + M)\),其中\(N\)為點數,\(M\)為邊數。
拓撲序列
有若干條有向邊(u, v)
,要求所有的u
均在線性序列中出現在對應的v
前,這樣的序列稱為拓撲序列。
形象地說,有若干件事情要做,做事件A前必須先完成事件B,做事件C前必須先完成事件D……拓撲序列就是做這些事情的順序。
算法思路
核心思路
我們可以將一條有向圖看做是一種約束關系,每個點的入度就是這個點被約束的次數。如果一個點被約束,則其不可以加入拓撲序列。
對於入度為0的點u
,u
此時沒有約束,則其可以加入拓撲序列中。同時,因為u
已經加入拓撲序列,其帶來的約束會消失。即刪除每條u
連出的有向邊(u, v)
(也就是刪除約束關系),同時v
的入度會-1
。
對於一個點v
來說,若其入度不為0,說明v
還有對應的u
在約束,所以v
不能加入拓撲序列;若其入度為0,說明v
所有對應的u
均已加入拓撲序列,所以v
可以加入拓撲序列。因此,拓撲排序可以分為三步:
- 找出入度為0的點
u
,並將其加入拓撲序列。 - 刪除
u
連出的每條有向邊(u, v)
。 - 重復第一步,直到圖中沒有入度為0的點為止。
算法結束后,如果加入拓撲序列的頂點數 <
圖中所有的頂點數,說明有向圖中存在環;否則,拓撲排序已經求出了該圖的一條拓撲序列。
如果需要求出字典序最小的拓撲序列,則存儲的數據結構需要換成小根堆的優先隊列。
建模思路
如果題目中給出了若干條約束關系:\(A\)必須與\(B\)……\(C\)必須與\(D\)……則這道題目大概率可以使用拓撲排序解決。使用拓撲排序的關鍵,在於正確的建圖。
我們可以把一條有向邊看做是一種關系。例如,如果u
的點權 <
v
的點權,我們可以將其看做是一條有向邊(u, v)
。又如,\(NOIP 2013\)的\(T4\)車站分級一題中,我們將一條有向邊(u, v)
看做是車站u
的級別 <
車站v
的級別。
建圖后,拓撲排序的解法就很容易得出了。
拓撲排序非常適合與\(\textbf{DAGdp}\)進行搭配使用。因為進行狀態轉移的時候,要求其的前驅狀態必須都已經計算完畢,與拓撲序列的性質不謀而合。因此,拓撲排序和記憶化搜索是實現\(DAGdp\)的最佳選擇。
對於一個有向圖\(G\),如果在拓撲排序的過程中,隊列里出現了多個元素,說明圖\(G\)的拓撲序不唯一,反之說明圖\(G\)的拓撲序是唯一的。因為當一個元素入隊后,其所有的前驅一定都已經出隊。如果此時隊列里有多個元素,則這些元素沒有前驅的約束,在之后的拓撲序中的位置可以安排。
拓撲排序有兩個非常重要的性質:一,假如在拓撲排序的過程中 ,隊列里出現了多於一個元素,說明該圖的拓撲序不唯一;二,假如拓撲序列的長度小於圖中的頂點數量,說明圖中有環。這兩個性質對解題有極大的作用。
模板
#include <cstdio>
#include <queue>
using namespace std;
#define maxn 1005
#define maxm 100005
struct node
{
int to, nxt;
}edge[maxm];
int n, m, cnt;
int head[maxn], indeg[maxn], res[maxn];
queue<int> q;
void topo()
{
for (int i = 1; i <= n; i++)
if (!indeg[i])
q.push(i);
while (!q.empty())
{
int v = q.front();
q.pop();
res[++cnt] = v;
for (int i = head[v]; i; i = edge[i].nxt)
{
indeg[edge[i].to]--;
if (!indeg[edge[i].to])
q.push(edge[i].to);
}
}
}
int main()
{
int x, y;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
scanf("%d%d", &x, &y);
edge[i].to = y;
edge[i].nxt = head[x];
head[x] = i;
indeg[y]++;
}
topo();
for (int i = 1; i <= cnt; i++)
printf("%d ", res[i]);
printf("\n");
return 0;
}
例題選講
最長鏈問題
給定一個有向無環圖\(G\),試求\(G\)中最長鏈的長度。
這道題是一個經典的\(DAGdp\)問題。令\(dp_{i}\)表示以頂點\(i\)結尾的最長鏈的長度。我們在拓撲排序的時候可以順便進行狀態轉移。因為如果拓撲排序的過程中,某個頂點\(u\)入度已經為\(0\),說明與\(u\)相連的頂點的\(dp\)值都已經計算完畢。此時,以\(u\)結尾的最長鏈的長度等於所有相鄰頂點的最長鏈長度加一,即\(dp_{u} = max(dp_{v} + 1), (u, v) \in E\)。最終取最大值就是該有向無環圖的最長鏈長度。