tarjan算法-解決有向圖中求強連通分量的利器


小引

看到這個名詞-tarjan,大家首先想到的肯定是又是一個以外國人名字命名的算法。說實話真的是很佩服那些算法大牛們,佩服得簡直是五體投地啊。今天就遇到一道與求解有向圖中強連通分量的問題,我的思路就是遍歷圖中的每一個點,然后進行深度遍歷,看最后能否回歸到這個點上。如果可以回歸,那么這個點肯定在一個強連通分量上。可是最后想着想着就亂了......

沒辦法,自己low啊,就百度了求有向圖中強連通分量的算法,於是乎tarjan算法出現在搜索結果上。

下面說一下,tarjan算法用到的一些圖的概念。

強連通圖、極大強連通子圖、強連通分量

我們知道,在有向圖G中,如果任意兩個頂點都是連通的(所謂連通就是兩個頂點都能互相到達),那么這個圖就是強連通圖。非強連通圖的極大強連通子圖,被稱為強連通分量。

那么什么是極大強連通子圖呢?

舉個例子幫助理解一下:例如下面圖一所示,其中子圖{1,2,3,5}就是一個極大強連通子圖,子圖{4}也是一個極大強連通子圖。

為什么會這樣呢?對於“極大”的理解,就是在一個局部子圖中不能再大。就像是數學中的求一個函數中的極大值和極小值一樣,例如求函數f(x)的極大值和極小值,變量x可以有不同的區間,所以在x的不同區間內就會有不同的極大值或極小值。

ok,我們看一下,首先這個有向圖是一個非連通的,對於子圖{4}而言,其自身可以到達自身,那么{4}是連通的,如果在點集{1,2,3,5}增加任何點,最后形成的圖卻是不連通的。所以說子圖{4}是原圖G的一個極大強連通子圖。那么對於子圖{1,2,3,5}的理解也是一樣的。

ok,對於tarjan涉及的基礎概念到此就介紹結束。下面正式的講一下tarjan算法。

tarjan算法

tarjan算法的基礎就是深度優先搜索-DFS。tarjan算法需要兩個數組進行輔佐,即low數組和dfn數組。dfn數組記錄搜索到該點時的時間(可以理解為該點被搜索的序號),low數組是一個標記數組,表示該點或者以這個點為根的子樹能夠追溯到最早的棧中節點的次序。

tarjan算法的操作原理

tarjan算法基於dfs算法,同一強連通分量內的所有頂點均在同一棵深度優先搜索樹中。也就是說強連通分量一定是有向圖的某個深度優先搜索生成樹。

用low值記錄點u所在強連通子圖對應的搜索子樹的根節點的dfs值。該子樹中的元素在棧中一定是相鄰的,且根節點在棧中一定位於所有子樹元素的最下方。

強連通分量是由若干個環組成,所以當有環形成時,我們將這一條路徑的low值統一,即這條路徑上的所有點屬於同一個強連通分量。

如果遍歷完整個搜索樹后某個點的dfn值等於low值,則它是該搜索子樹的根。這時,它以上(包括它自己)一直到棧頂的所有元素組成一個強連通分量。

tarjan算法的規則

數組初始化:當首次搜索到點u時,dfn和low數組的值都為到該點的時間。

堆棧:每遍歷到一個未被標記的點,將它入棧。

當點u可以到達點v時,如果點v不在棧中,那么low[u] = min{low[u],low[v]};如果點v在棧中,那么low[u] = min{low[u],dfn[v]}。

每當搜索到一個點並經過以上步驟后,其low值等於dfn值,則將它以及在它之上的元素彈出棧。這些出棧元素組成一個強連通分量。

繼續搜索(因為有向圖可能有多個連通子圖組成,而這些子圖沒有交集),直到所有點被遍歷。

tarjan算法演示

下面給出一個大牛寫的tarjan算法演示,很好,將tarjan算法的操作原理形象地表現了出來,可以很好地理解整個算法的執行過程。

從節點1開始DFS,把遍歷到的節點加入棧中。搜索到節點u=6時,DFN[6]=LOW[6],找到了一個強連通分量。退棧到u=v為止,{6}為一個強連通分量。

image

返回節點5,發現DFN[5]=LOW[5],退棧后{5}為一個強連通分量。

image

返回節點3,繼續搜索到節點4,把4加入堆棧。發現節點4向節點1有后向邊,節點1還在棧中,所以LOW[4]=1。節點6已經出棧,(4,6)是橫叉邊,返回3,(3,4)為樹枝邊,所以LOW[3]=LOW[4]=1。

image

繼續回到節點1,最后訪問節點2。訪問邊(2,4),4還在棧中,所以LOW[2]=DFN[4]=5。返回1后,發現DFN[1]=LOW[1],把棧中節點全部取出,組成一個連通分量{1,3,4,2}。

image

至此,算法結束。經過該算法,求出了圖中全部的三個強連通分量{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表示崇高的敬意。

附源碼

#include <iostream> #include <stack>
using namespace std; #define MAX_VERTEX_SIZE 10001
struct EdgeNode{ int vertex; EdgeNode *nextArc; }; struct VerTexNode{ EdgeNode* firstArc; }; struct Graph{ int n,e; VerTexNode vNode[MAX_VERTEX_SIZE]; }; int time = 0; int low[MAX_VERTEX_SIZE]; int dfn[MAX_VERTEX_SIZE]; int visited[MAX_VERTEX_SIZE]; int inStack[MAX_VERTEX_SIZE]; stack<int> st; Graph graph; void initeGraph(int n,int m) { for(int i = 1;i<=n;i++) { graph.vNode[i].firstArc = NULL; } graph.n = n; graph.e = m; } //頭插法建立圖
void creatGraph(int s,int v) { EdgeNode *edgeNode = new EdgeNode; edgeNode->vertex = v; edgeNode->nextArc = graph.vNode[s].firstArc; graph.vNode[s].firstArc = edgeNode; } int min(int a,int b) { if(a>b) return b; else
        return a; } void trajan(int u) { dfn[u] = low[u] = time++; st.push(u); visited[u] = 1; inStack[u] = 1; EdgeNode *edgePtr = graph.vNode[u].firstArc; while(edgePtr !=NULL) { int v = edgePtr->vertex; if(visited[v] == 0) { trajan(v); low[u] = min(low[u],low[v]); } else { low[u] = min(low[u],dfn[v]); } edgePtr = edgePtr->nextArc; } if(dfn[u] == low[u]) { int vtx; cout<<"set is: "; do{ vtx = st.top(); st.pop(); inStack[vtx] = 0;//表示已經出棧
            cout<<vtx<<' '; }while(vtx !=u ); } } int main() { int n,m; int s,a; cin>>n>>m; initeGraph(n,m); for(int i = 1;i<=n;i++) { visited[i] = 0; inStack[i] = 0; dfn[i] = 0; low[i] = 0; } for(int j = 1;j<=m;j++) { cin>>s>>a; creatGraph(s,a); } for(int i =1;i<=n;i++) if(visited[i] == 0) trajan(i); return 0; }
View Code


 


免責聲明!

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



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