拓撲排序


概念

拓撲排序

拓撲排序是一種在DAG上進行的算法。拓撲排序可以解決有若干對關系,需要求出滿足所有關系的一種方案/最大值/最小值的問題。拓撲排序可以求出圖中的一條拓撲序列。

拓撲排序可以檢查出有向圖中是否存在環,如果一個有向圖存在合法的拓撲序列,說明該有向圖內無環;反之,說明該有向圖中有環。拓撲排序是無法給有向帶環圖求出拓撲序列的。

拓撲排序的時間復雜度是\(O(N + M)\),其中\(N\)為點數,\(M\)為邊數。

拓撲序列

有若干條有向邊(u, v),要求所有的u均在線性序列中出現在對應的v前,這樣的序列稱為拓撲序列。
形象地說,有若干件事情要做,做事件A前必須先完成事件B,做事件C前必須先完成事件D……拓撲序列就是做這些事情的順序。

算法思路

核心思路

我們可以將一條有向圖看做是一種約束關系,每個點的入度就是這個點被約束的次數。如果一個點被約束,則其不可以加入拓撲序列。

對於入度為0的點uu此時沒有約束,則其可以加入拓撲序列中。同時,因為u已經加入拓撲序列,其帶來的約束會消失。即刪除每條u連出的有向邊(u, v)(也就是刪除約束關系),同時v的入度會-1

對於一個點v來說,若其入度不為0,說明v還有對應的u在約束,所以v不能加入拓撲序列;若其入度為0,說明v所有對應的u均已加入拓撲序列,所以v可以加入拓撲序列。因此,拓撲排序可以分為三步:

  1. 找出入度為0的點u,並將其加入拓撲序列。
  2. 刪除u連出的每條有向邊(u, v)
  3. 重復第一步,直到圖中沒有入度為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\)。最終取最大值就是該有向無環圖的最長鏈長度。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM