超詳細Tarjan演算法總結,求強連通分量,割點,割邊,有重邊的割邊
Tarjan是一個人,他一身中發明了很多演算法,就這幾個演算法最為出名。
1、求有向圖的強連通分量,那麼什麼是強連通分量呢,就是一個頂點集合,任意兩個頂點間都可以互相到達。一個頂點也是強聯通分量如果圖中任意兩點可以互相到達,則此圖強連通。下圖中頂點{1,0,2}屬於一個強聯通分量,{3},{4}也屬於;
TARJAN是基於dfs演算法的基礎上,所以也會得到一顆搜尋樹。如右邊圖
那麼他是怎麼執行的呢,首先大家看著兩個陣列
dfn[i],和low[i];一開始dfn【i】=low【i】=cnt++;
dfn[i]表示第i個節點深搜的順序。例如根據右圖深搜的順序可以得到dfn【1】=1;dfn【0】=2;dfn【2】=3;依次類推下去;
low【i】表示i節點能夠追溯到那個強聯通分量最先訪問的那個點的dfn值;因為他們是強聯通分量所以他們一定會有環。既low【2】=dfn【1】=1;low【0】=low【2】=dfn【1】=1; low【3】=dfn【3】=4;的值就不會改變因為她下面的點沒有邊能夠連線到他的祖先上去,也是因為沒有環。low【4】得值也不會改變;
大家此時有沒有發現一個規律 既 每個強聯通分量的第一個訪問的那個次序low【i】都會等於dfn【i】;這裡有三個想等所以他們有三個強聯通分量。
具體操作大家看下面配合程式碼的具體演示吧;
程式碼:
void Tarjan(int u)//此程式碼僅供參考
{
vis[u]=1;
low[u]=dfn[u]=cnt++;
for(int i=0;i<mp[u].size();i++)
{
int v=mp[u][i];
if(vis[v]==0){Tarjan(v);
low[u]=min(low[u],low[v]);}
else if(vis[v]==1) low【u】=min(low【u】,dfn【v】);
}
if(dfn[u]==low[u])
{
sig++;
}
}
程式碼模擬一遍哈!!! 請大家認真仔細的用筆寫,看哈;首先執行tarjan(u);此時u等於1;執行low【1】=dfn【1】=1;將這個頂點標記為已經訪問過
vis【1】=1;
節點0有沒有被訪問過,節點0沒有被訪問過往下面深搜此時搜到0,執行tarjan(0);執行low【0】=dfn【0】=2;繼續判斷
繼續深搜執行low【2】=dfn【2】=3;關鍵的來了哈,此時節點2繼續往下面深搜 搜到了節點1;但是1已經被訪問過了,就不繼續深搜了。執行low【2】=min(low【2】,dfn【1】);所以low【3】此時被賦值為1;然後第三層tarjan的迴圈語句結束了。執行判斷dfn【3】是不是等於low【3】;這裡是不等於所以計數器不增加;現在返回到第二層tarjan此時剛執行完tarjan【2】;接著更新low【0】則執行low【0】=min(low【0】,low【2】);現在繼續迴圈v=3;判斷v有沒有被訪問過這裡是沒有被訪問過執行tarjan(3);接著low【3】=dfn【3】=4;然後進入迴圈體 v=4;然後節點4沒有被訪問過執行tarjan(4);low【4】=dfn【4】=5;接著進入迴圈體,因為下面沒有節點與4相連則退出迴圈體;則執行判斷low【4】==dfn【4】此時他們想等計數器加1;接著返回到tarja(3);執行low【3】=min(low【3】,low【4】)因為節點4沒有往上面去的邊,所以low【4】還是等於4並且大於low【3】;此時的low【3】也是同理不變;此時這個迴圈體也執行完了此時判斷low【3】==dfn【3】這裡是想等的所以計數器增加1;
此時函式返回到tarjan(0);在執行low【0】=min(low【3】);此時low【0】=1小於low【3】;則不更新low【0】;現在迴圈體也執行完了判斷low【0】==dfn【0】;這裡是不相等的;計數器不加;返回到tarjan(1);此時他的迴圈體執行完了判斷low【1】==dfn【1】;這裡是想等計數器加1;最後求得強聯通分量的個數為3;然後返回到主函式;以上就是tarjan演算法的執行過程;
那麼大家有沒有想過萬一圖不連通呢;
所以我們在外面的這樣處理 在外面加入一個迴圈;
程式碼;
#include<stdio.h>//此程式碼僅供參考,用於求一個圖存在多少個強連通分量
#include<string.h>
#include<vector>
#include<algorithm>
using namespace std;
#define maxn 1000000
vector<int >mp[maxn];
int ans[maxn];
int vis[maxn];
int dfn[maxn];
int low[maxn];
int n,m,tt,cnt,sig;
void init()
{
memset(low,0,sizeof(low));
memset(dfn,0,sizeof(dfn));
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++)mp[i].clear();
}
void Tarjan(int u)
{
vis[u]=1;
low[u]=dfn[u]=cnt++;
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++;
}
}
void Slove()
{
tt=-1;cnt=1;sig=0;
for(int i=1;i<=n;i++)
{
if(vis[i]==0)
{
Tarjan(i);
}
}
printf("%d\n",sig);
}
int main()
{
while(~scanf("%d",&n))
{
if(n==0)break;
scanf("%d",&m);
init();
for(int i=0;i<m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
mp[x].push_back(y);
}
Slove();
}
}
這裡求強聯通分量就將講到這裡了;
推薦大家入門題目;hdu迷宮城堡;傳送門http://blog.csdn.net/lw277232240/article/details/72123936
下面我們求割點;
割點什麼是割點呢;就是在一個連通圖中刪除一個點就導致真個圖不連通了,那麼這個點就叫做割點;下圖實邊為dfs搜尋的邊,虛線邊為回邊,既可以訪問父親的祖先的邊;
例如下圖的A就是一個割點;
該演算法是R.Tarjan發明的。觀察DFS搜尋樹,我們可以發現有兩類節點可以成為割點:
- 對根節點u,若其有兩棵或兩棵以上的子樹,則該根結點u為割點;
- 對非葉子節點u(非根節點),若其子樹的節點均沒有指向u的祖先節點的回邊,說明刪除u之後,根結點與u的子樹的節點不再連通;則節點u為割點。
對於根結點,顯然很好處理;但是對於非葉子節點,怎麼去判斷有沒有回邊是一個值得深思的問題。
我們用dfn[u]
記錄節點u在DFS過程中被遍歷到的次序號,low[u]
記錄節點u或u的子樹通過非父子邊追溯到最早的祖先節點(即DFS次序號最小),這裡的dfn
low 跟上面的功能是一樣的。,那麼low[u]的計算過程如下:
下表給出圖(a)對應的dfn與low陣列值。
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
vertex | A | B | C | D | E | F | G | H | I | J | K | L | M |
dfn[i] | 1 | 5 | 12 | 10 | 11 | 13 | 8 | 6 | 9 | 4 | 7 | 2 | 3 |
low[i] | 1 | 1 | 1 | 5 | 5 | 1 | 5 | 5 | 8 | 2 | 5 | 1 | 1 |
對於情況2,當(u,v)為樹邊且low[v]
>= dfn[u]
時,節點u才為割點。該式子的含義:以節點v為根的子樹所能追溯到最早的祖先節點要麼為v要麼為u。
程式碼:
void tarjan(int u,int pre)
{
dfn[u]=low[u]=cnt++;
vis[u]=1;
for(int i=0;i<G[u].size();i++)
{ int a=G[u][i];
if(a==pre) continue;
if(!vis[a])
{tarjan(a,u);
low[u]=min(low[u],low[a]);
if(u==pre) {gen++;}//當他為跟的時候
else if(low[a]>=dfn[u]) cut[u]++;//這裡是不是跟 切low[a]>=dfn[u],那麼這個點u就是割點,這裡是統計割點的個數就執行cut【u】++
}
else {
low[u]=min(low[u],dfn[a]);
}
}
if(pre==u) cut[pre]=gen-1;
}