拓撲排序(topsort)詳解
這篇隨筆就信息學奧林匹克競賽中圖論的一個知識點——拓撲排序進行講解。拓撲排序的內容比較基礎,只要求讀者學習過並了解信息學中圖的相關定義和一些專業名詞,但是拓撲排序的變形題目比較多,希望讀者在看完本隨筆后認真體會練習,掌握拓撲排序。
上課!
拓撲排序的定義
顧名思義,這是一種排序,確切地說,是一種圖上排序,在一張有向無環圖(注解:有向無環圖即很多參考書和題解中所說的DAG)上進行排序,把其中的所有節點排成一個序列,使得圖中的任意一對有邊相連的節點(u,v)u要出現在v前。
所以我再次強調,拓撲排序只能用在有向無環圖中!!
這樣的線性序列我們稱之為拓撲序。
注意,拓撲序不唯一!這個地方不明白的請自己畫圖理解(或者參考下面的那棵樹)。
拓撲排序的實現原理
在講解圖的拓撲排序之前,我們可以用一棵樹來加深對拓撲排序的理解(因為樹是絕對沒有環)。
我們隨意地定義一棵有向樹(如下圖),如果我們想得到它的拓撲序,那會很簡單,只需要先把根節點8號放進隊列中,然后再放8號的任意一個兒子節點,繼續此操作。直到節點全放進去為止。
我們會發現,問放進去的是任意的一個子節點,所以我們說拓撲序是不唯一的(在絕大多數情況下,你要非跟我抬杠說假如只有一條鏈,我也沒辦法)。
拓撲排序的實現
講完了實現原理,我們來進行拓撲排序的代碼實現,根據上面的原理,我們會發現,我們要保證拓撲序列的正確性,只需要把圖中的入度為0的節點先放進拓撲序,然后把這個點和它所有的出邊全部刪掉,這樣就還會出現一些入度為0的點,我們繼續重復以上操作。
有細心的小伙伴會發現這個和算法中的寬搜很相似,沒錯,所謂寬搜和深搜,都是基於對樹與圖的深度/寬度優先遍歷而定義的,所以拓撲排序的實現其實就是借助了寬搜的思想。
上模板:
void topsort()
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(rudu[j]==0)
{
x=j;
top[++cnt]=j;
rudu[j]--;
break;
}
}
for(int j=head[x];j;j=nxt[j])
rudu[j]--;
}
}
以上代碼的top數組存拓撲序列,使用的是鏈式前向星存圖並遍歷。比較好理解,但是時間復雜度比較低。(所以僅供理解)
所以我們用C++STL來實現拓撲排序,這樣會快很多。
模板:
void topsort()
{
queue<int> q;
for(int i=1;i<=n;i++)
if(rudu[i]==0)
q.push(i);
while(q.empty())
{
int x=q.front();
q.pop();
top[++cnt]=x;
for(int i=head[x];i;i=nxt[i])
{
int y=to[i];
rudu[y]--;
if(rudu[y]==0)
q.push(y);
}
}
}
其實也很好理解啦...
注意,以上代碼是針對於已經保證圖是DAG的情況下而出現的,假如我們沒有題目中DAG的保證,就要額外地判這個圖是不是DAG,即很多題目中要求的“無解”情況。
怎么判斷呢?
學到這里,我覺得你應該想到,如果最后得到的拓撲序列的長度等於節點總數,那么這個圖就是DAG,否則就不是。
所以我們最后進行判斷。
(你也可以使用STL中的vector容器)
代碼:
if(cnt==n)
//DAG操作
else
//非DAG操作
拓撲排序的用途及一些技巧
拓撲排序的用途是解決一些依賴關系的題,一般來講沒有圖論的基本要素(告訴你幾個點,一眼就看出來這是一道圖論題%%%),所以,我認為做拓撲排序題的難點在於如何建立一個和題意相符的圖(建圖坑死爹)。所以美其名曰拓撲排序是圖論中最簡單的內容,其實它的相關題目都很有思維含量,所以強烈建議各位同學多刷題多刷題。
由於拓撲排序不唯一,所以有些坑爹題目要求拓撲序列的一些內容,比如按字典序等等。
這時我們把原本的隊列拓撲排序換成優先隊列拓撲排序。
注意,優先隊列不能提速,不要以為找到了一份更好的模板,一定要讀題~~!!
除了定義方式有點怪異其他的跟隊列一樣。
priority_queue<int,vector<int>,greater<int> >q;
//取隊首的時候需要變成q.top();