詳解tarjan求強聯通分量
tarjan演算法是在有向圖中求強聯通分量的一種演算法,基於dfs
其中最重要的需要維護的兩個陣列是low[maxn],dfn[maxn]
low[u]代表u可以到達的最近節點,dfn[u]代表u在dfs樹中的深度
其原理是
1.若一個點u是強聯通分量的根節點,那麼這個點在dfs中的遍歷順序dfn[u]的大小一定會等於它可以到達的最小節點的遍歷順序low[u],即這個點最短只能自己到自己
2.若一個點u不是強聯通分量的根節點,那麼它可以到達的最小節點的遍歷順序low[u]一定小於他自身的遍歷順序大小dfn[u]
換句話說,在dfs到一個強聯通分量時,第一個被dfs到的點的dfs序一定比這個強聯通分量裡的其他點的dfs序小,於是我們就可以把這個強連通分量看作一棵樹,用染色的方式來將它們全部找出來
注意兩個概念:
1.這裡講到的遍歷順序是指用dfs以某個確定節點為起點遍歷整個圖時訪問到某個節點的順序,如根節點的dfn為1,根節點下一層的節點的dfn都為2
2.上面提到的近是用dfn來衡量的,即dfn越小,點越近
好了,明白了原理之後,演算法框架就可以構建了
假設要找點u屬於的強聯通分量中有哪些點
1.管你怎麼辦,反正首先存一個圖
2.然後以u為起點,將u放入一個棧中,標記u在棧中
3.對於u的每一個子節點v構成的邊(u,v),都有三種情況:要麼是一條樹邊,要麼是一條返祖邊,要麼是一條橫叉邊,故我們可以對樹邊和返祖邊進行處理,而橫叉邊不用考慮(若在同一個分量中就不可能構成橫叉邊了),於是對於樹邊(特徵是沒有被訪問過),就先dfs再用low[u]和low[v]的較小值來更新low[u],對於返祖邊(特徵是在棧中),就用low[u]和dfn[v]的較小值來更新low[u]
4.如果u是根節點,即dfn[u]==low[u],那就意味著棧中u上面的節點都是u的聯通點,於是將u上面的點都彈出來放在陣列中就可以得到u的強連通分量點了
相關程式碼如下
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX 100010
int dfsnum[MAX],dfsNum,low[MAX];
int sccnum[MAX],sccNum;
int instack[MAX],st[MAX],top;
typedef struct EDGE
{
int v,next;
}edge;
edge e[MAX];
int edgeNum;
int head[MAX];
void insertEdge(int a,int b)
{
e[edgeNum].v=b;
e[edgeNum].next=head[a];
head[a]=edgeNum++;
}
void Tarjan(int i)//找i的強聯通分量
{
dfsnum[i]=low[i]=++dfsNum;//時間戳,最近訪問節點為dfsNum
st[top++]=i;//入棧
instack[i]=1;//標記入棧
int j=head[i];
for(j=head[i];j!=-1;j=e[j].next)//遍歷i的子節點
{
int v=e[j].v;//v是i的子節點
if(dfsnum[v]==0)//為樹邊,因為v沒有經過dfs
{
Tarjan(v);//dfs v
if(low[i]>low[v])//其實就是更新low值,最近訪問節點值
low[i]=low[v];//可改成low[i]=min(low[i],low[v]);
}
else if(instack[v])//如果是反祖邊的話,就意味著i的low值可能會變小
{
if(low[i]>dfsnum[v])
low[i]=dfsnum[v];//low[i]=min(low[i],dfsnum[v]);
}
}
if(dfsnum[i]==low[i])//如果i是根節點,馬上吐棧
{
do
{
top--;
sccnum[st[top]]=sccNum;
instack[st[top]]=0;
}while(top>=0&&st[top]!=i);
sccNum++;
}
}
void solve(int n)//n有用?
{
int i;
memset(dfsnum,0,sizeof(dfsnum));
memset(instack,0,sizeof(instack));
dfsNum=0;
top=0;
sccNum=0;
for(i=1;i<=n;i++)
{
if(dfsnum[i]==0)//如果沒有進入分量圖,tarjan(i),找i的強聯通分量
Tarjan(i);
}
}
int main()
{
int n,m;
int a,b,i;
while(scanf("%d %d",&n,&m))
{
if(m==0&&n==0)
break;
memset(head,-1,sizeof(head));
edgeNum=0;
for(i=0;i<m;i++)
{
scanf("%d %d",&a,&b);
insertEdge(a,b);//插邊
}
solve(n);
if(sccNum==1)
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
tarjan這個人很厲害,他發明了3個演算法,分別可以求強聯通分量,橋和割點
斷點
若點u是斷點,顯然它後面的點的low陣列都比割點的low陣列大
橋
若low[v]>dfn[u],則(u,v)為割邊。但是實際處理時我們並不這樣判斷,因為有的圖上可能有重邊,這樣不好處理。我們記錄每條邊的標號(一條無向邊拆成的兩條有向邊標號相同),記錄每個點的父親到它的邊的標號,如果邊(u,v)是v的父親邊,就不能用dfn[u]更新low[v]。這樣如果遍歷完v的所有子節點後,發現low[v]=dfn[v],說明u的父親邊(u,v)為割邊。
給道題吧:
天凱是蘇聯的總書記。蘇聯有n個城市,某些城市之間修築了公路。任意兩個城市都可以通過公路直接或者間接到達。
天凱發現有些公路被毀壞之後會造成某兩個城市之間無法互相通過公路到達。這樣的公路就被稱為dangerous pavement。
為了防止美帝國對dangerous pavement進行轟炸,造成某些城市的地面運輸中斷,天凱決定在所有的dangerous pavement駐紮重兵。可是到底哪些是dangerous pavement呢?你的任務就是找出所有這樣的公路。
Input format:
第一行n,m(1<=n<=150, 1<=m<=5000),分別表示有n個城市,總共m條公路。
以下m行每行兩個整數a, b,表示城市a和城市b之間修築了直接的公路。
Output format:
輸出有若干行。每行包含兩個數字a,b(a<b),表示<a,b>是dangerous pavement。請注意:輸出時,所有的數對<a,b>必須按照a從小到大排序輸出;如果a相同,則根據b從小到大排序。
Sample:
6 6
1 2
2 3
2 4
3 5
4 5
5 6
Dager.out
1 2
5 6
code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=150+5;
const int maxm=5000+5;
bool G[maxn][maxn];
int low[maxn],dfn[maxn],DFN=0;
int n,m;
int numofgroup=0,pointer=0;
struct node{
int u;
int v;
};
node ans[maxm];
void datasetting()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)memset(G[i],0,sizeof(G[i]));
int u,v;
for(int i=0;i<m;i++)
{
scanf("%d%d",&u,&v);
G[u][v]=G[v][u]=true;
}
}
void tarjan(int u,int fa)
{
low[u]=dfn[u]=++DFN;
for(int v=1;v<=n;v++)
{
if(G[u][v])
{
if(!dfn[v])
{
tarjan(v,u);
low[u]=min(low[v],low[u]);
if(dfn[u]<low[v])
{
node a;
a.u=u;a.v=v;
ans[pointer++]=a;
}
}
else if(v!=fa)low[u]=min(low[u],dfn[v]); }
}
}
bool cmp(node a,node b)
{
if(a.u==b.u)return a.v<b.v;
return a.u<b.u;
}
int main()
{
datasetting();
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,i);
sort(ans,ans+pointer,cmp);
for(int i=0;i<pointer;i++)
{
node a=ans[i];
printf("%d %d\n",a.u,a.v);
}
return 0;
}