理解
在有向圖G中,如果兩點互相可達,則稱這兩個點強連通,如果G中任意兩點互相可達,則稱G是強連通圖。
定理: 1、一個有向圖是強連通的,當且僅當G中有一個回路,它至少包含每個節點一次。
2、非強連通有向圖的極大強連通子圖,稱為強連通分量(SCC即Strongly Connected Componenet)。
在上圖中,{1,2,3,4}是一個強連通分量,{5},{6}分別是另外兩個強連通分量。怎么判斷一個圖是否是強連通圖,如果不是,有哪些強連通分量,又怎么使它成為強連通圖呢?
方法1:Korasaju算法
首先理解一下轉置圖的定義:將有向圖G中的每一條邊反向形成的圖稱為G的轉置G T 。(注意到原圖和G T 的強連通分支是一樣的)
算法流程:
1.深度優先遍歷G,算出每個結點u的結束時間f[u],起點如何選擇無所謂。
2.深度優先遍歷G的轉置圖G T ,選擇遍歷的起點時,按照結點的結束時間從大到小進行。遍歷的過程中,一邊遍歷,一邊給結點做分類標記,每找到一個新的起點,分類標記值就加1。
3. 第2步中產生的標記值相同的結點構成深度優先森林中的一棵樹,也即一個強連通分量
注意:
Kosaraju算法比Tarjan時間復雜度要高,應用范圍小,還有着爆棧超內存的風險,但這個算法比Tarjan好理解很多。當然和Tarjan一樣,Kosaraju也只能用於有向圖中。
Kosaraju也是基於深度優先搜索的算法。這個算法牽扯到兩個概念,發現時間st,完成時間et。發現時間是指一個節點第一次被遍歷到時的次序號,完成時間是指某一結點最后一次被遍歷到的次序號。
在加邊時把有向圖正向建造完畢后再反向加邊建一張逆圖。
先對正圖進行一遍dfs,遇到沒訪問過的點就讓其發現時間等於目前的dfs次序號。在回溯時若發現某一結點的子樹全部被遍歷完,就讓其完成時間等於目前dfs次序號。正圖遍歷完后將節點按完成時間入棧,保證棧頂是完成時間最大的節點,棧底是完成時間最小的節點。然后從棧頂開始向下每一個沒有被反向遍歷過的節點為起點對逆圖進行一遍dfs,將訪問到的點記錄下來(或染色)並彈棧,每一遍反向dfs遍歷到的點就構成一個強連通分量。
圖解:
(a)為有向圖G,
其中的陰影部分
是G的強連通分
支,對每個頂點
都標出了其發現
時刻與完成時刻
,黑色邊為深度
優先搜索的樹
枝;
(b)G的轉置圖G T
依次以b,c,g,h
為起點做DFS,
得到4個強連通
分量
算法復雜度分析
深度優先搜索的復雜度:Θ(V + E)
計算G T 的復雜度:0或者Θ(V + E)(臨接表)
所以總的復雜度為:Θ(V + E)
非常好的算法!(個人更青睞於Tarjan,但kosaraju的思路至少得理解)
模板:
void positive_dfs(int pos){ DFN++; vis[pos]=1; for(int i=pre[1][pos];i;i=E[1][i].next) if(!vis[E[1][i].to]) positive_dfs(E[1][i].to); stack[N*2+1-(++DFN)]=pos; } void negative_dfs(int pos){ dye[pos]=CN; vis[pos]=0; size[dye[pos]]++; for(int i=pre[2][pos];i;i=E[2][i].next) if(vis[E[2][i].to]) negative_dfs(E[2][i].to); } int main(){ ...... for(int i=1;i<=N;i++) if(!vis[i]) positive_dfs(i); for(int i=1;i<=N*2;i++) if(stack[i]&&vis[stack[i]]){ CN++; negative_dfs(stack[i]); } ...... }
方法二:Tarjan算法
理解:
Tarjan算法是基於對圖深度優先搜索的算法,每個強連通分量為搜索樹中的一棵子樹。總的來說, Tarjan算法基於一個觀察,即:同處於一個SCC中的結點必然構成DFS樹的一棵子樹。 我們要找SCC,就得找到它在DFS樹上的根。
算法思想如下:
dfn[u]表示dfs時達到頂點u的次序號(時間戳),low[u]表示以u為根節點的dfs樹中次序號最小的頂點的次序號,所以當dfn[u]=low[u]時,以u為根的搜索子樹上所有節點是一個強連通分量。 先將頂點u入棧,dfn[u]=low[u]=++idx,掃描u能到達的頂點v,如果v沒有被訪問過,則dfs(v),low[u]=min(low[u],low[v]),如果v在棧里,low[u]=min(low[u],dfn[v]),掃描完v以后,如果dfn[u]=low[u],則將u及其以上頂點出棧。
圖解(一定要仔細從左往右看):
模板(Tarjan算法):
void tarjan(int pos){ vis[stack[++index]=pos]=1;//入棧並標記 LOW[pos]=DFN[pos]=++dfs_num; for(int i=pre[pos];i;i=E[i].next){ if(!DFN[E[i].to]){ tarjan(E[i].to); LOW[pos]=min(LOW[pos],LOW[E[i].to]); } else if(vis[E[i].to]) LOW[pos]=min(LOW[pos],DFN[E[i].to]); } if(LOW[pos]==DFN[pos]){ vis[pos]=0; size[dye[pos]=++CN]++;//染色及記錄強連通分量大小 while(pos!=stack[index]){ vis[stack[index]]=0; size[CN]++;//記錄大小 dye[stack[index--]]=CN;//彈棧並染色 } index--; } }
模板(完整Tarjan):
#include <cstdio> #include <stack> #include <cstring> #include <iostream> using namespace std; int n,m,idx=0,k=1,Bcnt=0; int head[100]; int ins[100]={0}; int dfn[100]={0},low[100]={0}; int Belong[100]; stack <int> s; struct edge { int v,next; }e[100]; int min(int a,int b) { return a<b?a:b; } void adde(int u,int v) { e[k].v=v; e[k].next=head[u]; head[u]=k++; } void readdata() { int a,b; memset(head,-1,sizeof(head)); scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { scanf("%d%d",&a,&b); adde(a,b); } } void tarjan(int u) { int v; dfn[u]=low[u]=++idx;//每次dfs,u的次序號增加1 s.push(u);//將u入棧 ins[u]=1;//標記u在棧內 for(int i=head[u];i!=-1;i=e[i].next)//訪問從u出發的邊 { v=e[i].v; if(!dfn[v])//如果v沒被處理過 { tarjan(v);//dfs(v) low[u]=min(low[u],low[v]);//u點能到達的最小次序號是它自己能到達點的最小次序號和連接點v能到達點的最小次序號中較小的 } else if(ins[v])low[u]=min(low[u],dfn[v]);//如果v在棧內,u點能到達的最小次序號是它自己能到達點的最小次序號和v的次序號中較小的 } if(dfn[u]==low[u]) { Bcnt++; do { v=s.top(); s.pop(); ins[v]=0; Belong[v]=Bcnt; }while(u != v); } } void work() { for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i); printf("\n"); for(int i = 1;i <= 6;i++)printf("%d %d\n",dfn[i],low[i]); printf("共有%d強連通分量,它們是:\n",Bcnt); for(int i=1;i<=Bcnt;i++) { printf("第%d個:",i); for(int j=1;j<=n;j++) { if(Belong[j]==i)printf("%d ",j); } printf("\n"); } } int main() { readdata(); work(); return 0; } /* 6 8 1 2 1 3 2 4 3 4 3 5 4 1 4 6 5 6 */
至於例題,~~博主太懶,自己去找吧,推薦codevs1332 上白澤慧音和洛谷 受歡迎的牛