【转】BYV--有向图强连通分量的Tarjan算法
轉自beyond the void 的博客:
https://www.byvoid.com/zhs/blog/scc-tarjan
注:紅色為標注部分
[有向圖強連通分量]
在有向圖G中,如果兩個頂點間至少存在一條路徑,稱兩個頂點強連通(strongly connected)。如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖。非強連通圖有向圖的極大強連通子圖,稱為強連通分量(strongly connected components)。
下圖中,子圖{1,2,3,4}為一個強連通分量,因為頂點1,2,3,4兩兩可達。{5},{6}也分別是兩個強連通分量。
直接根據定義,用雙向遍歷取交集的方法求強連通分量,時間復雜度為O(N^2+M)。更好的方法是Kosaraju算法或Tarjan算法,兩者的時間復雜度都是O(N+M)。本文介紹的是Tarjan算法。
[Tarjan算法]
Tarjan算法是基于對圖深度優先搜索的算法,每個強連通分量為搜索樹中的一棵子樹。搜索時,把當前搜索樹中未處理的節點加入一個堆棧,回溯時可以判斷棧頂到棧中的節點是否為一個強連通分量。
定義DFN(u)為節點u搜索的次序編號(時間戳),Low(u)為u或u的子樹能夠追溯到的最早的棧中節點的次序號。由定義可以得出,
Low(u)=Min
{DFN(u),Low(v),(u,v)為樹枝邊,u為v的父節點DFN(v),(u,v)為指向棧中節點的后向邊(非橫叉邊)
}
當DFN(u)=Low(u)時,以u為根的搜索子樹上所有節點是一個強連通分量。
?如果一個節點u已經DFS訪問結束,而且此時其low值等于dfn值,則說明u可達的所有節點,都不能到達任何在u之前被DFS訪問的節點 ---- 那么該節點u就是一個強連通分量在DFS搜索樹中的根。
算法偽代碼如下
tarjan(u)
{DFN[u]=Low[u]=++Index // 為節點u設定次序編號和Low初值Stack.push(u) // 將節點u壓入棧中for each (u, v) in E // 枚舉每一條邊if (v is not visted) // 如果節點v未被訪問過tarjan(v) // 繼續向下找Low[u] = min(Low[u], Low[v])else if (v in S) // 如果節點v還在棧內Low[u] = min(Low[u], DFN[v])if (DFN[u] == Low[u]) // 如果節點u是強連通分量的根
repeatv = S.pop // 將v退棧,為該強連通分量中一個頂點
print vuntil (u== v)
}
接下來是對算法流程的演示。
從節點1開始DFS,把遍歷到的節點加入棧中。搜索到節點u=6時,DFN[6]=LOW[6],找到了一個強連通分量。退棧到u=v為止,{6}為一個強連通分量。
返回節點5,發現DFN[5]=LOW[5],退棧后{5}為一個強連通分量。
返回節點3,繼續搜索到節點4,把4加入堆棧。發現節點4向節點1有后向邊,節點1還在棧中,所以LOW[4]=1。節點6已經出棧,(4,6)是橫叉邊,返回3,(3,4)為樹枝邊,所以LOW[3]=LOW[4]=1。
繼續回到節點1,最后訪問節點2。訪問邊(2,4),4還在棧中,所以LOW[2]=DFN[4]=5。返回1后,發現DFN[1]=LOW[1],把棧中節點全部取出,組成一個連通分量{1,3,4,2}。
至此,算法結束。經過該算法,求出了圖中全部的三個強連通分量{1,3,4,2},{5},{6}。
可以發現,運行Tarjan算法的過程中,每個頂點都被訪問了一次,且只進出了一次堆棧,每條邊也只被訪問了一次,所以該算法的時間復雜度為O(N+M)。
求有向圖的強連通分量還有一個強有力的算法,為Kosaraju算法。Kosaraju是基于對有向圖及其逆圖兩次DFS的方法,其時間復雜度也是O(N+M)。與Trajan算法相比,Kosaraju算法可能會稍微更直觀一些。但是Tarjan只用對原圖進行一次DFS,不用建立逆圖,更簡潔。在實際的測試中,Tarjan算法的運行效率也比Kosaraju算法高30%左右。此外,該Tarjan算法與求無向圖的雙連通分量(割點、橋)的Tarjan算法也有著很深的聯系。學習該Tarjan算法,也有助于深入理解求雙連通分量的Tarjan算法,兩者可以類比、組合理解。
求有向圖的強連通分量的Tarjan算法是以其發明者Robert Tarjan命名的。Robert Tarjan還發明了求雙連通分量的Tarjan算法,以及求最近公共祖先的離線Tarjan算法,在此對Tarjan表示崇高的敬意。
附:tarjan算法的C++程序
void tarjan(int i)
{int j;DFN[i]=LOW[i]=++Dindex;instack[i]=true;Stap[++Stop]=i;for (edge *e=V[i];e;e=e->next){j=e->t;if (!DFN[j]){tarjan(j);if (LOW[j]<LOW[i])LOW[i]=LOW[j];}else if (instack[j] && DFN[j]<LOW[i])LOW[i]=DFN[j];}if (DFN[i]==LOW[i]){Bcnt++;do{j=Stap[Stop--];instack[j]=false;Belong[j]=Bcnt;}while (j!=i);}
}
void solve()
{int i;Stop=Bcnt=Dindex=0;memset(DFN,0,sizeof(DFN));for (i=1;i<=N;i++)if (!DFN[i])tarjan(i);
}
[參考資料]
- Wikipedia
- Amber的圖論總結
BYVoid 原創作品,轉載請注明。
?
?思想: http://www.cnblogs.com/c1299401227/p/5402414.html
?
?做一遍DFS,用dfn[i]表示編號為i的節點在DFS過程中的訪問序號(也可以叫做開始時間)用low[i]表示i節點DFS過程中i的下方節點所能到達的開始時間最早的節點的開始時間。(也就是之后的深搜所能到達的最小開始時間)初始時dfn[i]=low[i]
?
?在DFS過程中會形成一搜索樹。在搜索樹上越先遍歷到的節點,顯然dfn的值就越小。
?
?DFS過程中,碰到哪個節點,就將哪個節點入棧。棧中節點只有在其所屬的強連通分量已經全部求出時,才會出棧。
?(對于一個強連通分量和一個指出去的有向邊,在輸出這個強連通分量的時候,那條指出去的邊的終點已經作為一個強連通分量統計,并且出棧了。)
?如果發現某節點u有邊連到搜索樹(棧)中棧里的節點v,則更新u的low 值為dfn[v](更新為low[v]也可以,更新為low可以算是壓縮路徑,速度略快)。
?
?如果一個節點u已經DFS訪問結束,而且此時其low值等于dfn值,則說明u可達的所有節點,都不能到達任何在u之前被DFS訪問的節點 ---- 那么該節點u就是一個強連通分量在DFS搜索樹中的根。
?
另外附一篇資料,寫的也很好:
處理SCC(強連通分量問題)的Tarjan算法
---Comzyh
https://comzyh.com/blog/archives/517/
在有向圖G中,如果兩個頂點間至少存在一條路徑,稱兩個頂點強連通(strongly connected),如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖.
如圖所示,藍色框圈起來的是一個強連通分量
通俗的說法是:從圖G內任意一個點出發,存在通向圖G內任意一點的的一條路徑.
非強連通圖有向圖的極大強連通子圖,稱為強連通分量(strongly connected components,SCC).
求圖強連通分量的意義是:由于強連通分量內部的節點性質相同,可以將一個強連通分量內的節點縮成一個點,即消除了環,這樣,原圖就變成了一個有向無環圖(directed acyclic graph,DAG).顯然對于一個無向圖,求強連通分量沒有什么意義,聯通即為強連通.
求強連通分量比較高效的算法是SCC Tarjan算法,BYV牛有一個很好的說明,推薦大家看一看:有向圖強連通分量的Tarjan算法? Beyond the Void,我在這里就不照搬了.
Tarjan 算法基本基于DFS,時間復雜度就是遍歷圖一遍,為Θ(N),Tarjan 貌似很喜歡深搜的樣子,LCA被深搜活生生的弄成了Θ(N),SCC 看來一樣,Tarjan 一出現,時間復雜度果然降了一個數量級.
先看BYV牛的CODE,寫的真不錯,雖然第一遍我沒看懂,不過相信加了注釋后會好理解多,如果有錯誤,別打我.
void tarjan(int i)//Tarjan
{int j;DFN[i]=LOW[i]=++Dindex;//Index 時間戳 instack[i]=true;//標記入棧 Stap[++Stop]=i;//入棧 for (edge *e=V[i];e;e=e->next)//枚舉所有相連邊
{j=e->t;//臨時變量 if (!DFN[j])//j沒有被搜索過
{tarjan(j);//遞歸搜索j if (LOW[j]<LOW[i])//回溯中發現j找到了更老的時間戳 LOW[i]=LOW[j];//更新能達到老時間戳
}else if (instack[j] && DFN[j]<LOW[i])//如果已經印有時間戳 且 時間戳比較小,則有環 LOW[i]=DFN[j];//當前記錄可追溯時間戳更新
}if (DFN[i]==LOW[i])//可追溯最老是自己,表明自己是當前強連通分量的棧底
{Bcnt++;//強連通分量數增加 do{j=Stap[Stop--];//出棧頂元素 instack[j]=false;//標記出棧 Belong[j]=Bcnt;//記錄j所在的強連通分量編號
}while (j!=i);//如果是當前元素,彈棧完成
}
}
void solve()
{int i;Stop=Bcnt=Dindex=0;memset(DFN,0,sizeof(DFN));//標記為為搜索過 for (i=1;i<=N;i++)if (!DFN[i])tarjan(i);
}
SCC Tarjan算法的基本框架:
- 遍歷一個點,指定一個唯一時間戳DFN[i];指定該點向前追溯可追溯到的最老時間戳LOW[i].
- 枚舉當前點所有邊,若DFN[j]=0表明未被搜索過,遞歸搜索之.
- 若DFN[j]不為0,則j被搜索過,這時判斷j是否在棧中,且j的時間戳DFN[j]小于當前時間戳DFN[i],可判定成環.將LOW[i]設定為DFN[j]
- 若這個點LOW[i]和DFN[i]相等,說明這個節點是所有強連通分量的元素中在棧中最早的節點.
- 彈棧,將這個強連通分量全部彈出,縮成點.
這里解釋下比較晦澀的兩個詞:
LOW[i],這個向前追溯可追溯到的最老時間戳 的意思是根據有向圖的方向遍歷,在有環的情況下,向前追溯可以達到較早的點.在每次遞歸調用Tarjan(j)后我們可以高速較快的更新LOW[i]
至于所有強連通分量的元素中在棧中最早的節點.是這樣的,所有自成強連通分量的節點或子圖,在本次彈棧前都已經彈出去了,這時棧里所元素的LOW都大于等于DFN[該強連通分量的最早遍歷節點],棧頂元素一定比下面元素的時間戳要大
在遍歷完所有相關邊之后再驗證(DFN[i]==LOW[i]),可以避免有漏判情況,這時,當前節點的子節點已經將LOW更新的很優了,不存在一個強連通分量被包含在一個強連通分量里的情況.
學習SCC Tarjan算法有兩道測驗題:
POJ 2186 Popular Cows
POJ 1236 Network of Schools
參見Comzyh這兩道強連通分量的題解
原創文章,轉載請注明(最好把圖片帶走): 轉載自Comzyh的博客
本文鏈接地址: 處理SCC(強連通分量問題)的Tarjan算法
轉載于:https://www.cnblogs.com/liuzhanshan/p/6517041.html
閱讀世界,共赴山海423全民讀書節,邀你共讀
總結
以上是生活随笔為你收集整理的【转】BYV--有向图强连通分量的Tarjan算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux安装grpc占用空间大,grp
- 下一篇: Linux网络模块全局变量,()不是Li