Tarjan算法 詳解+心得


  Tarjan算法是由Robert Tarjan(羅伯特·塔揚,不知有幾位大神讀對過這個名字) 發明的求有向圖中強連通分量的算法。

  預備知識:有向圖,強連通。

  有向圖:由有向邊的構成的圖。需要注意的是這是Tarjan算法的前提和條件。

  強連通:如果兩個頂點可以相互通達,則稱兩個頂點 強連通(strongly connected)。如果有向圖G的每兩個頂點都 強連通,稱G是一個強連通圖。非 強連通圖有向圖的極大強連通子圖,稱為強連通分量(strongly connected components)。

  For example:

  

  在這個有向圖中1、2、3、4四個點可以互相到達,就稱這四個點組成的子圖為強連通分量。且這四個點兩兩強連通。

  然后就可以開始學習神奇的Tarjan算法了!

  Tarjan算法是用來求強連通分量的,它是一種基於DFS(深度優先搜索)的算法,每個強連通分量為搜索樹中的一棵子樹。並且運用了數據結構棧。

  在介紹詳細原理前,先引入兩個非常重要的數組:dfn[ ] 與 low[ ]

  dfn[ ]:就是一個時間戳(被搜到的次序),一旦某個點被DFS到后,這個時間戳就不再改變(且每個點只有唯一的時間戳)。所以常根據dfn的值來判斷是否需要進行進一步的深搜。

  low[ ]:該子樹中,且仍在棧中的最小時間戳,像是確立了一個關系,low[ ]相等的點在同一強連通分量中。

  注意初始化時 dfn[ ] = low[ ] = ++cnt.

  

  算法思路:

  首先這個圖不一定是一個連通圖,所以跑Tarjan時要枚舉每個點,若dfn[ ] == 0,進行深搜。

  然后對於搜到的點尋找與其有邊相連的點,判斷這些點是否已經被搜索過,若沒有,則進行搜索。若該點已經入棧,說明形成了環,則更新low.

  在不斷深搜的過程中如果沒有路可走了(出邊遍歷完了),那么就進行回溯,回溯時不斷比較low[ ],去最小的low值。如果dfn[x]==low[x]則x可以看作是某一強連通分量子樹的根,也說明找到了一個強連通分量,然后對棧進行彈出操作,直到x被彈出。

  先來一波局部代碼加深一下理解:

void tarjan(int now)
{
    dfn[now]=low[now]=++cnt;  //初始化
    stack[++t]=now;       //入棧操作
    v[now]=1;            //v[]代表該點是否已入棧
    for(int i=f[now];i!=-1;i=e[i].next)  //鄰接表存圖
        if(!dfn[e[i].v])           //判斷該點是否被搜索過
        {
            tarjan(e[i].v);
            low[now]=min(low[now],low[e[i].v]); //回溯時更新low[ ],取最小值
        }
        else if(v[e[i].v])
            low[now]=min(low[now],dfn[e[i].v]); //一旦遇到已入棧的點,就將該點作為連通量的根
                             //這里用dfn[e[i].v]更新的原因是:這個點可能
                             //已經在另一個強連通分量中了但暫時尚未出棧,所
                             //以now不一定能到達low[e[i].v]但一定能到達
                             //dfn[e[i].v].
    if(dfn[now]==low[now])
    {
        int cur;
        do
        {
            cur=stack[t--];
            v[cur]=false;                       //不要忘記出棧
        }while(now!=cur);
    }
}

手動模擬一下過程:

從1進入 dfn[1]= low[1]= ++cnt = 1
入棧 1
由1進入2 dfn[2]=low[2]= ++cnt = 2
入棧 1 2
之后由2進入4 dfn[4]=low[4]= ++cnt = 3
入棧 1 2 4
之后由4進入 6 dfn[6]=low[6]=++cnt = 4
入棧 1 2 4 6

 6無出度,之后判斷 dfn[6]==low[6]
說明6是個強連通分量的根節點:6及6以后的點出棧並輸出。

回溯到4后發現4找到了一個已經在棧中的點1,更新 low [ 4 ] = min ( low [ 4 ] , dfn [ 1 ] )

於是 low [ 4 ] = 1 .

由4繼續回到2 Low[2] = min ( low [ 2 ] , low [ 4 ] ).
low[2]=1;
由2繼續回到1 判斷 low[1] = min ( low [ 1 ] ,  low [ 2 ] ). 
low[1]還是 1
然后更新3的過程省略,大家可以自己手動模擬一下。

。。。。。。。。。

省略了1->3的更新過程之后,1的所有出邊就跑完了

於是判斷:low [ 1 ] == dfn [ 1 ] 說明以1為根節點的強連通分量已經找完了。

將棧中1以及1之后進棧的所有點,都出棧並輸出。 

End

完整代碼如下:

#include<iostream>  //輸出所有強連通分量
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

int n,m,x,y,top=0,cnt=0,t,col;
int ans1=-1,ans2=-1,ans3=-1;
int d[200020];
int a[200020];
int c[200020];
int f[200020];
int dfn[200020];
int low[200020];
int stack[200020];

bool v[200020];

struct edge{
    int u;
    int v;
    int w;
    int next;
}e[1000020];

void Add(int u,int v,int w)
{
    ++top;
    e[top].u=u;
    e[top].v=v;
    e[top].w=w;
    e[top].next=f[u];
    f[u]=top;
}

int read()
{
    int x=0;
    int k=1;
    char c=getchar();
    while(c>'9'||c<'0')
    {
        if(c=='-') k=-1;
        c=getchar();
    }
    while(c>='0'&&c<='9')
        x=x*10+c-'0',
        c=getchar();
    return x*k;
}

void tarjan(int now)
{
    dfn[now]=low[now]=++cnt;
    stack[++t]=now;
    v[now]=1;
    for(int i=f[now];i!=-1;i=e[i].next)
        if(!dfn[e[i].v]) 
        {
            tarjan(e[i].v);
            low[now]=min(low[now],low[e[i].v]);
        }
        else if(v[e[i].v])
            low[now]=min(low[now],dfn[e[i].v]);    
    int cur;
    if(dfn[now]==low[now])
    {
        do
        {
            cur=stack[t--];
            v[cur]=false;
            printf("%d ",cur);
        }while(now!=cur);
        printf("\n");
    }
}

int main()
{
    n=read();
    m=read();
    memset(f,-1,sizeof f);
    for(int i=1;i<=n;++i)
        a[i]=read();
    for(int i=1;i<=m;++i)
    {
        x=read();
        y=read();
        Add(x,y,0);
    }
    for(int i=1;i<=n;++i)
        if(!dfn[i]) tarjan(i);
    return 0;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM