1. 程式人生 > >強連通演算法--Tarjan演算法

強連通演算法--Tarjan演算法

首先我們引入定義:

1、有向圖G中,以頂點v為起點的弧的數目稱為v的出度,記做deg+(v);以頂點v為終點的弧的數目稱為v的入度,記做deg-(v)。

2、如果在有向圖G中,有一條<u,v>有向道路,則v稱為u可達的,或者說,從u可達v。

3、如果有向圖G的任意兩個頂點都互相可達,則稱圖 G是強連通圖,如果有向圖G存在兩頂點u和v使得u不能到v,或者v不能到u,則稱圖G是強非連通圖。

4、如果有向圖G不是強連通圖,他的子圖G2是強連通圖,點v屬於G2,任意包含v的強連通子圖也是G2的子圖,則乘G2是有向圖G的極大強連通子圖,也稱強連通分量。

5、什麼是強連通?強連通其實就是指圖中有兩點u,v。使得能夠找到有向路徑從u到v並且也能夠找到有向路徑從v到u,則稱u,v是強連通的。

然後我們理解定義:

既然我們現在已經瞭解了什麼是強連通,和什麼是強連通分量,可能大家對於定義還是理解的不透徹,我們不妨引入一個圖加強大家對強連通分量和強連通的理解:
這裡寫圖片描述

標註棕色線條框框的三個部分就分別是一個強連通分量,也就是說,這個圖中的強連通分量有3個。

其中我們分析最左邊三個點的這部分:

其中1能夠到達0,0也能夠通過經過2的路徑到達1.1和0就是強連通的。

其中1能夠通過0到達2,2也能夠到達1,那麼1和2就是強連通的。

同理,我們能夠看得出來這一部分確實是強連通分量,也就是說,強連通分量裡邊的任意兩個點,都是互相可達的。

那麼如何求強連通分量的個數呢?另外強連通演算法能夠實現什麼一些基本操作呢?我們繼續詳解、

接著我們開始接觸演算法,討論如何用Tarjan演算法求強連通分量個數:

Tarjan演算法,是一個基於Dfs的演算法(如果大家還不知道什麼是Dfs,自行百度學習),假設我們要先從0號節點開始Dfs,我們發現一次Dfs我萌就能遍歷整個圖(樹),而且我們發現,在Dfs的過程中,我們深搜到 了其他強連通分量中,那麼俺們Dfs之後如何判斷他喵的哪個和那些節點屬於一個強連通分量呢?我們首先引入兩個陣列:

①dfn【】

②low【】

第一個陣列dfn我們用來標記當前節點在深搜過程中是第幾個遍歷到的點。第二個陣列是整個演算法核心陣列,我們稍後再說,這個時候我們不妨在紙上畫一畫寫一寫,搞出隨意一個Dfs出來的dfn陣列來觀察一下(假設我們從節點0開始的Dfs,其中一種可能的結果是這樣滴):

這裡寫圖片描述

這個時候我們回頭來看第二個陣列要怎樣操作,我們定義low【u】=min(low【u】,low【v】(即使v搜過了也要進行這步操作,但是v一定要在棧內才行)),u代表當前節點,v代表其能到達的節點。這個陣列在剛剛到達節點u的時候初始化:low【u】=dfn【u】。然後在進行下一層深搜之後回溯回來的時候,維護low【u】。如果我們發現了某個節點回溯之後的low【u】值還是==dfn【u】的值,那麼這個節點無疑就是一個關鍵節點:從這個節點能夠到達其強連通分量中的其他節點,但是沒有其他屬於這個強連通分量以外的點能夠到達這個點,所以這個點的low【u】值維護完了之後還是和dfn【u】的值一樣,口述可能理解還是相對費勁一些,我們走一遍流程圖:

①首先進入0號節點,初始化其low【0】=dfn【0】=1,然後深搜到節點2,初始化其:low【2】=dfn【2】=2,然後深搜到節點1,初始化其:low【1】=dfn【1】=3;

②然後從節點1開始繼續深搜,發現0號節點已經搜過了,沒有繼續能夠搜的點了,開始回溯維護其值。low【1】=min(low【1】,low【0】)=1;low【2】=min(low【2】,low【1】)=1;low【0】=min(low【0】,low【2】)=1;

③這個時候猛然發現,low【0】==dfn【0】,這個時候不要太開心,就斷定一定0號節點是一個關鍵點,別忘了,這個時候還有3號節點沒有遍歷,我們只有在其能夠到達的節點全部判斷完之後,才能夠下結論,所以我們繼續Dfs。

④繼續深搜到3號節點,初始化其low【3】=dfn【3】=4,然後深搜到4號節點,初始化其:low【4】=dfn【4】=5,這個時候發現深搜到底,回溯,因為節點4沒有能夠到達的點,所以low【4】也就沒有幸進行維護即:low【4】=dfn【4】(這個點一定是強連通分量的關鍵點,但是我們先忽略這個點,這個點沒有代表性,一會分析關鍵點的問題),然後回溯到3號節點,low【3】=min(low【3】,low【4】)=4;發現low【3】==dfn【3】那麼這個點也是個關鍵點,我們同樣忽略掉。

⑤最終回溯到節點0,進行最後一次值的維護:low【0】=min(low【0】,low【3】)=0,這個時候我們猛然發現其dfn【0】==low【0】,根據剛才所述,那麼這個點就是一個關鍵點:能夠遍歷其屬強連通分量的點的起始點,而且沒有其他點屬於其他強連通分量能夠有一條有向路徑連到這個節點來的節點。

※※大家仔細理解一下這句話,因為這個點屬於一個強連通分量,而且強連通分量中的任意兩個節點都是互達的,也就是說強連通分量中一定存在環,這個最後能夠回到0號節點的1號節點一定有機會維護low【1】,因為0號節點是先進來的,所以其low【1】的值也一定會跟著變小,然後在回溯的過程中,其屬一個強連通分量的所有點都會將low【u】值維護成low【0】,所以這個0號節點就是這個關鍵點:能夠遍歷其屬強連通分量的起始點而且這樣的起始點一定只有一個,所以只要發現了一個這樣的關鍵起始點,那麼就一定發現了一個強連通分量。而且這個節點沒有其他點屬於其他強連通分量能夠有一條有向路徑連到這個節點來的節點:如果這樣的點存在,那麼這些個點應該屬於同一個強連通分量。

那麼綜上所述,相信大家也就能夠理解為什麼dfn【u】==low【u】的時候,我們就可以判斷我們發現了一個強連通分量了。

程式碼實現:

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++;  
    }  
}  


然後再給一份完整程式碼,附加兩組資料,大家可以參考一下:

#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();  
    }  
}  
/* 
7 9 
1 2 
2 3 
3 1 
2 4 
4 7 
7 4 
4 5 
5 6 
6 4 
8 10 
1 2 
2 3 
3 1 
2 4 
4 7 
7 4 
4 5 
5 6 
6 4 
7 8 
*/  

接下來我們討論一下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);  
    }  
}