求強連通分量的tarjan算法
強連通分量:是有向圖中的概念,在一個圖的子圖中,任意兩個點相互可達,也就是存在互通的路徑,那麼這個子圖就是強連通分量。(如果一個有向圖的任意兩個點相互可達,那麼這個圖就稱爲強連通圖)。
如果u是某個強連通分量的根,那麼:
(1)u不存在路徑可以返回到它的祖先。
(2)u的子樹也不存在路徑可以返回到u的祖先。
· 例如:
· 強連通分量。在一個非強連通圖中極大的強連通子圖就是該圖的強連通分量。比如圖中子圖{1,2,3,5}是一個強連通分量,子圖{4}是一個強連通分量。
tarjan算法的基礎是深度優先搜索,用兩個數組low和dfn,和一個棧。low數組是一個標記數組,記錄該點所在的強連通子圖所在搜索子樹的根節點的dfn值,dfn數組記錄搜索到該點的時間,也就是第幾個搜索這個點的。根據以下幾條規則,經過搜索遍歷該圖和對棧的操作,我們就可以得到該有向圖的強連通分量。
算法規則:
· 數組的初始化:當首次搜索到點p時,Dfn與Low數組的值都爲到該點的時間。
· 堆棧:每搜索到一個點,將它壓入棧頂。
· 當點p有與點p’相連時,如果此時(時間爲dfn[p]時)p’不在棧中,p的low值爲兩點的low值中較小的一個。
· 當點p有與點p’相連時,如果此時(時間爲dfn[p]時)p’在棧中,p的low值爲p的low值和p’的dfn值中較小的一個。
· 每當搜索到一個點經過以上操作後(也就是子樹已經全部遍歷)的low值等於dfn值,則將它以及在它之上的元素彈出棧。這些出棧的元素組成一個強連通分量。
· 繼續搜索(或許會更換搜索的起點,因爲整個有向圖可能分爲兩個不連通的部分),直到所有點被遍歷。
算法僞代碼:
tarjan(u)
{
DFN[u]=Low[u]=++Index // 爲節點u設定次序編號和Low初值
Stack.push(u) // 將節點u壓入 棧中
for each (u, v) in E // 枚舉每一條邊
if (!dfn[v]) // 如果節點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是強連通分量的根
do{
v = S.pop // 將v退棧,爲該強連通分量中一個頂點
}while(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)。
此外,該Tarjan算法與求無向圖的雙連通分量(割點、橋)的Tarjan算法也有着很深的聯繫。學習該Tarjan算法,也有助於深入理解求雙連通分量的Tarjan算法,兩者可以類比、組合理解。
應用例子:(OJ 1484 popular cows)
題意:有N只牛,輸入a,b的話,則說明b關注a,而且關注有傳遞性。例如c關注b,且b關注a,則c也關注a。題目問有多少隻奶牛能被其他所有的奶牛關注。
把題目的模型轉換:N個頂點的有向圖,有M條邊。求一共有多少個點,滿足這樣的條件:所有其它的點都可以到達這個點。這個點滿足條件的充要條件是:這個點是樹中唯一的出度爲0的點。
先求強連通分量,然後可以把強連通分量縮成一個點,因爲,在強連通分量中的任意兩個點可以到達,所有的點具有相同的性質,即它們分別能到達的點集都是相同的,能夠到達它們的點集也是相同的。然後就重新構圖,縮點後的圖是沒有強連通分量的,否則,可將環上所有點也縮成一個點,與極大強聯通分量矛盾。所以只要找出度爲0的點,並且出度爲0的點只有1個 ,如果出度爲0的點有多個的話,問題則無解。
代碼:
#include<stdio.h>
#include<string.h>
#define adj 10010
#define edg 50010
struct node
{
int v;
int next;
};
node edge[edg];
node edge1[edg];
int low[adj],dfn[adj],Stack[adj];
int first[adj],first1[adj],fuck[adj];
bool ins[adj];
int n,m,temp,cnt,top,count;
int cnt_size[adj],belong[adj],outdegree[adj];
void creat(int u,int v)
{
edge1[count].next=first1[u];
edge1[count].v=v;
first1[u]=count++;
}
void tarjan(int u)
{
int i,v;
dfn[u]=low[u]=++temp;
Stack[top++]=u;
ins[u]=true;
for(i=first[u];i!=-1;i=edge[i].next)
{
v=edge[i].v;
if(!dfn[v])
{
tarjan(v);
if(low[u]>low[v])
low[u]=low[v];
}
else if(ins[v])
{
if(low[u]>dfn[v])
low[u]=dfn[v];
}
}
if(dfn[u]==low[u])
{
int j;
do
{
top--;
j=Stack[top];
ins[j]=false;
cnt_size[cnt]++;
belong[j]=cnt;
}while(u!=j);
cnt++;
}
}
int main()
{
int i,j,k,v,sum,num;
int e=0;
scanf("%d%d",&n,&m);
memset(first,-1,sizeof(first));
for(k=0;k<m;k++) 建立圖
{
scanf("%d%d",&i,&j);
edge[e].v=j;
edge[e].next=first[i];
first[i]=e;
e++;
}
memset(dfn,0,sizeof(dfn));
memset(ins,false,sizeof(ins));
temp=cnt=top=0;
memset(cnt_size,0,sizeof(cnt_size));
memset(low,0,sizeof(low));
for(i=1;i<=n;i++) 求強連通分量
{
if(!dfn[i])
tarjan(i);
}
memset(first1,-1,sizeof(first1));
count=0;
for(i=1;i<=n;i++) 重新構造圖
{
for(j=first[i];j!=-1;j=edge[j].next)
{
v=edge[j].v;
if(belong[i]!=belong[v])
creat(belong[i],belong[v]);
}
}
memset(outdegree,0,sizeof(outdegree));
for(i=0;i<cnt;i++) 求每個節點的出度
{
for(j=first1[i];j!=-1;j=edge1[j].next)
outdegree[i]++;
}
sum=num=0;
for(i=0;i<cnt;i++)
{
if(outdegree[i]==0) 求節點爲0的個數
{
sum=cnt_size[i];
num++;
}
}
if(num==1)
printf("%d\n",sum);
else
printf("0\n");
return 0;
}
Gabow算法
[編輯] 求解有向圖強連通分量的Gabow算法
Gabow算法與Tarjan算法的核心思想實質上是相通的,就是利用強連通分量必定是DFS的一棵子樹這個重要性質,通過找出這個子樹的根來求解強分量.具體到實現是利用一個棧S來保存DFS遇到的所有樹邊的另一端頂點,在找出強分量子樹的根之後,彈出S中的頂點一一進行編號. 二者不同的是,Tarjan算法通過一個low數組來維護各個頂點能到達的最小前序編號,而Gabow算法通過維護另一個棧來取代low數組,將前序編號值更大的頂點都彈出,然後通過棧頂的那個頂點來判斷是否找到強分量子樹的根
int Gabow(Graph G) { // 初始DFS用到的全局變量 S = StackInit(G->V); // S用來保存所有結點 P = StackInit(G->V); // P用來維護路勁 int v; for (v = 0; v < G->V; ++v) pre[v] = G->sc[v] = -1; cnt = id = 0; // DFS for (v = 0; v < G->V; ++v) if (pre[v] == -1) GabowDFS(G, v); // 釋放棧空間 StackDestroy(S); StackDestroy(P); return id; // 返回id的值,這恰好是強連通分量的個數 } void GabowDFS(Graph G, int w) { Link t; int v; pre[w] = cnt++; // 對前序編號編號 StackPush(S, w); // 講路徑上遇到的樹邊頂點入棧 StackPush(P, w); for (t = G->adj[w]; t; t = t->next) { if (pre[v = t->v] == -1) // 如果當前頂點以前未遇到,則對其進行DFS GabowDFS(G, v); else if (G->sc[v] == - 1) // 否則如果當前頂點不屬於強分量 while (pre[StackTop(P)] > pre[v]) // 就將路徑棧P中大於當前頂點pre值的頂點都彈出 StackPop(P); } if (StackTop(P) == w) { // 如果P棧頂元素等於w,則找到強分量的根,就是w StackPop(P); do { v = StackPop(S); // 把S中的頂點彈出編號 G->sc[v] = id; } while (v != w); ++id; } }