筆記:Tarjan算法 求解有向圖強連通分量的線性時間的算法
Tarjan他爾賤算法 求解有向圖強連通分量的線性時間的算法
百度百科 https://baike.baidu.com/item/tarjan%E7%AE%97%E6%B3%95/10687825?fr=aladdin
參考博文
http://blog.csdn.net/qq_34374664/article/details/77488976
http://blog.csdn.net/mengxiang000000/article/details/51672725
https://www.cnblogs.com/shadowland/p/5872257.html
算法介紹(基於DFS算法)
了解tarjan算法之前你需要知道:強連通,強連通圖,強連通分量。
強 連 通:如果兩個頂點可以相互通達,則稱兩個頂點強連通(strongly connected)。
在一個有向圖G裏,設兩個點a和b, 發現由a有一條路可以走到b,由b又有一條 路可以走到a,我們就叫這兩個頂點(a,b)強連通。(註意間接連接也可以)
強連通圖:如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖。
強連通分量:在一個有向圖G中,有一個子圖G2,這個子圖G2每2個點都滿足強連通,我們就 把這個子圖G2叫做強連通分量 。
我們來看一個有向圖方便理解:
標註藍色線條框框的部分就是一個強連通分量,節點3也是一個強連通分量
我們再來看一個圖(百度百科中的圖):
子圖{1,2,3,4}
Tarjan算法是基於對圖深度優先搜索的算法,每個強連通分量實際上為搜索樹中的一棵子樹。搜索時,把當前搜索樹中未處理的節點加入到一個堆棧,回溯時可以判斷棧頂到棧中的節點是否為一個強連通分量。
在Tarjan算法中,有如下定義。
(註意,下面的定義如看不明白沒關系,多看後面的模擬就明白了)
DFN[u]數組 : 在DFS中該節點被搜索的次序編號,每個點的次序編號都不一樣。
通俗地解釋DFN[u]: 意思就是在DFS的過程中,當前的這個節點是第幾個被遍歷到的點
LOW[u]數組 : u或u的子樹能夠追溯到的最早的棧中節點的次序號
通俗地解釋LOW[u]: 就是在DFS的過程中,如果當前節點是極大強聯通子圖的話,他的根節點的標號就是對應的LOW值:
當DFN[ u ]==LOW[ u ]時,u或u的子樹可以構成一個強連通分量。
通俗地解釋:當DFN[ u ]==LOW[ u ]時,以u為根的搜索子樹上所有節點是一個強連通分量。
回溯條件: DFS遇到的節點在已在棧中或者DFS遇到無出度的節點時就回溯。
回溯時需要維護LOW[u]的值。
如果下一個要遍歷的節點在棧中,那麽就要把當前節點的LOW[u]值設置成下一個節點(在棧中)的DFN[v]值。如:LOW[u]=DFN[v] 或者LOW[u]= min(LOW[u], DFN[v])
如果還需要接著回溯,那麽接著維護LOW[u]=min(LOW[u],LOW[v])
(即使v搜過了也要進行這步操作,但是v一定要在棧內才行)
u代表當前節點,v代表其能到達的節點。在進行一層深搜之後回溯的時候,維護LOW[u]的值。如果我們發現了某個節點回溯之後維護的LOW[u]值還是==DFN[u]的值,那麽這個節點無疑就是一個關鍵節點:
算法演示:以1為Tarjan 算法的起始點,如圖:前面不明白沒關系,重點從這裏開始看
假如從1號節點開始遍歷,開始dfs,並不斷設置當前節點的DFN值和LOW值,並把當前這個節點壓入棧中,那麽第一次在節點6處停止,因為6沒有出度。
那麽此時的DFN和LOW值分別為:
從1開始: DFN[1]=LOW[1]= ++index ----1
入棧 1
由1進入3: DFN[3]=LOW[3]= ++index ----2
入棧 1 3
由3進入5: DFN[5]=LOW[5]= ++index ----3
入棧 1 3 5
由5進入6: DFN[6]=LOW[6]= ++index ----4
入棧 1 3 5 6
可以用下圖來表示:
因為節點6無出度,於是判斷 DFN[6]==LOW[6],把6出棧(pop)。
{6}是一個強連通分量。
目前棧的節點有: 1 3 5 見下圖:
之後回溯到節點5,節點6被訪問過了並出棧(pop)了,所以它也沒有能訪問的邊了,
那麽 DFN[5]==LOW[5],{5} 也是一個強連通分量,彈出5
目前棧的節點有: 1 3
返回節點3,繼續搜索到節點4,節點4是新節點,設DFN[4]=LOW[4]=5並把4加入堆棧。
DFN[4]=LOW[4]= ++index -----5
入棧 1 3 4 見下圖:
繼續節點4往下找,找到了節點1 。
因為1號節點還在棧中,那麽就代表著棧中的現有的所有元素構成了一個強連通圖
(仔細想想、、兜了一圈又回到起點1)
回溯到節點4,更新 LOW[4]的值: LOW[4]= min(LOW[4], DFN[1]) 值更新為1
再接著訪問4的下一個節點6,節點6 被訪問過並POP了,就不用管它了。
再回溯到節點3,更新 LOW[3]的值: LOW[3]= min(LOW[3], LOW[4]) 值更新為1
3號節點也沒有能訪問的下一個節點了。圖如下:
再回溯到節點1,更新 LOW[1]的值: LOW[1]= min(LOW[1], LOW[3]) 值還是為1
節點1還有邊沒有走過。發現節點2,訪問節點2,節點2是新節點,放入棧中
DFN[2]=LOW[2]=++index ----6
入棧 1 3 4 2
由節點2,走到4,發現4被訪問過了,4還在棧裏,
回溯到節點2 更新LOW[2] = min(LOW[2], DFN[4]) LOW[2]=5
節點2也沒有可訪問的下一個節點了。
再回溯到節點1 更新LOW[1] = min(LOW[1], LOW[2]) LOW[1]=1
這時我們發現LOW[1]==DFN[1] 說明以1為 根節點 的強連通分量已經找完了。
將棧中1,3,4,2全部節點都出棧。{1,3,4,2} 是強連通分量。圖如下
至此,算法結束。經過該算法,求出了圖中全部的三個強連通分量{1,3,4,2}, {5}, {6}。
可以發現,運行Tarjan算法的過程中,每個頂點都被訪問了一次,且只進出了一次堆棧,每條邊也只被訪問了一次,所以該算法的時間復雜度為O(N+M)。
實戰:(後面有Tarjan算法的偽代碼及模板,可以參考)
P1726 上白澤慧音 https://www.luogu.org/problemnew/show/1726
P2661 信息傳遞 https://www.luogu.org/problemnew/show/2661
P3379 LCA Tarjan算法 https://www.luogu.org/problemnew/show/3379
P1262 間諜網絡 (提示:可用Tanjan縮點) https://www.luogu.org/problemnew/show/1262
P3387 【模板】縮點 https://www.luogu.org/problemnew/show/3387
以下4道題是北京大學的是英文題,如不明白意思可看下面的翻譯:
題解 http://blog.csdn.net/u012469987/article/details/51292558#poj-1236-network-of-schools
http://poj.org/problem?id=2186 http://poj.org/problem?id=1236
http://poj.org/problem?id=1904 http://poj.org/problem?id=1330
接下來我們討論一下Tarjan算法另外能夠幹一些什麽:
既然我們知道,Tarjan算法相當於在一個有向圖中找有向環,那麽我們Tarjan算法最直接的能力就是縮點辣!縮點基於一種染色實現,我們在Dfs的過程中,嘗試把屬於同一個強連通分量的點都染成一個顏色,那麽同一個顏色的點,就相當於一個點。
比如剛才的實例圖中縮點之後就可以變成這樣:
將一個有向帶環圖變成了一個有向無環圖(DAG圖)。很多算法要基於有向無環圖才能進行的算法就需要使用Tarjan算法實現染色縮點,建一個DAG圖然後再進行算法處理。在這種場合,Tarjan算法就有了很大的用武之地!
那麽這個時候 ,我們再引入一個數組color【i】表示節點i的顏色,再引入一個數組stack【】實現一個棧,然後在Dfs過程中每一次遇到點都將點入棧,在每一次遇到關鍵點的時候將棧內元素彈出,一直彈到棧頂元素是關鍵點的時候為止,對這些彈出來的元素進行染色即可。
縮點代碼實現:
void Tarjan(int u) //此代碼僅供參考 { vis[u]=1; low[u]=dfn[u]=cnt++; stack[++tt]=u; for(int i=0;i<mp[u].size();i++) { int v=mp[u][i]; if(vis[v]==0)Tarjan(v); if(vis[v]==1)low[u]=min(low[u],low[v]); } if(dfn[u]==low[u]) { sig++; do { low[stack[tt]]=sig; color[stack[tt]]=sig; vis[stack[tt]]=-1; } while(stack[tt--]!=u); } }
Tarjan算法偽代碼參考:
//註意,v指的是u能達到的下一個節點 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) // 如果節點u還在棧內 Low[u] = min(Low[u], DFN[v]) if (DFN[u] == Low[u]) // 如果節點u是強連通分量的根 repeat v = S.pop // 將v退棧,為該強連通分量中一個頂點 print v until (u== v) }
Tanjan算法模板:
void Tarjan ( int x ) { dfn[ x ] = ++dfs_num ; low[ x ] = dfs_num ; vis [ x ] = true ; //是否在棧中 stack [ ++top ] = x ; for ( int i=head[ x ] ; i!=0 ; i=e[i].next ) { int temp = e[ i ].to ; if ( !dfn[ temp ] ) { Tarjan ( temp ) ; low[ x ] = gmin ( low[ x ] , low[ temp ] ) ; } else if ( vis[ temp ])low[ x ] = gmin ( low[ x ] , dfn[ temp ] ) ; } if ( dfn[ x ]==low[ x ] ) //構成強連通分量 { vis[ x ] = false ; color[ x ] = ++col_num ; //染色 while ( stack[ top ] != x ) //清空 { color [stack[ top ]] = col_num ; vis [ stack[ top-- ] ] = false ; } top -- ; } }
Tanjan算法另一個模板:
#define M 5010 //題目中可能的最大點數 int STACK[M],top=0; //Tarjan算法中的棧 bool InStack[M]; //檢查是否在棧中 int DFN[M]; //深度優先搜索訪問次序 int Low[M]; //能追溯到的最早的次序 int ComponentNumber=0; //有向圖強連通分量個數 int Index=0; //索引號 vector<int> Edge[M]; //鄰接表表示 vector<int> Component[M]; //獲得強連通分量結果 int InComponent[M]; //記錄每個點在第幾號強連通分量裏 int ComponentDegree[M]; //記錄每個強連通分量的度 void Tarjan(int i) { int j; DFN[i]=Low[i]=Index++; InStack[i]=true;STACK[++top]=i; for (int e=0;e<Edge[i].size();e++) { j=Edge[i][e]; if (DFN[j]==-1) { Tarjan(j); Low[i]=min(Low[i],Low[j]); } else if (InStack[j]) Low[i]=min(Low[i],DFN[j]); } if (DFN[i]==Low[i]) { ComponentNumber++; do{ j=STACK[top--]; InStack[j]=false; Component[ComponentNumber]. push_back(j); InComponent[j]=ComponentNumber; } while (j!=i); } }
Tarjan算法裸代碼:
輸入:
一個圖有向圖。
輸出:
它每個強連通分量。
這個圖就是剛才講的那個圖。一模一樣。
input:
6 8
1 3
1 2
2 4
3 4
3 5
4 6
4 1
5 6
output:
6
5
3 4 2 1
#include<cstdio> #include<algorithm> #include<string.h> using namespace std; struct node { int v,next; }edge[1001]; int DFN[1001],LOW[1001]; int stack[1001],heads[1001],visit[1001],cnt,tot,index; void add(int x,int y) { edge[++cnt].next=heads[x]; edge[cnt].v = y; heads[x]=cnt; return ; } void tarjan(int x) //代表第幾個點在處理。遞歸的是點。 { DFN[x]=LOW[x]=++tot; //新進點的初始化。 stack[++index]=x; //進棧 visit[x]=1; //表示在棧裏 for(int i=heads[x];i!=-1;i=edge[i].next) { if(!DFN[edge[i].v]) //如果沒訪問過 { tarjan(edge[i].v); //往下進行延伸,開始遞歸 LOW[x]=min(LOW[x],LOW[edge[i].v]);//遞歸出來,比較誰是誰的兒子/父親,就是樹的對應關系,涉及到強連通分量子樹最小根的事情。 } else if(visit[edge[i].v ])//如果訪問過,並且還在棧裏 { LOW[x]=min(LOW[x],DFN[edge[i].v]);//比較誰是誰的兒子/父親。就是鏈接對應關系 } } if(LOW[x]==DFN[x]) //發現是整個強連通分量子樹裏的最小根。 { do{ printf("%d ",stack[index]); visit[stack[index]]=0; index--; }while(x!=stack[index+1]);//出棧,並且輸出。 printf("\n"); } return ; } int main() { memset(heads,-1,sizeof(heads)); int n,m; scanf("%d%d",&n,&m); int x,y; for(int i=1;i<=m;i++) { scanf("%d%d",&x,&y); add(x,y); } for(int i=1;i<=n;i++) if(!DFN[i]) tarjan(i);//當這個點沒有訪問過,就從此點開始。防止圖沒走完 return 0; }
筆記:Tarjan算法 求解有向圖強連通分量的線性時間的算法