1. 程式人生 > >筆記:Tarjan算法 求解有向圖強連通分量的線性時間的算法

筆記:Tarjan算法 求解有向圖強連通分量的線性時間的算法

true fff lan number lock 無環 還需 sin 第一次

Tarjan他爾賤算法 求解有向圖強連通分量的線性時間的算法

百度百科 https://baike.baidu.com/item/tarjan%E7%AE%97%E6%B3%95/10687825?fr=aladdin

參考博文

http://blog.csdn.net/qq_34374664/article/details/77488976

http://blog.csdn.net/mengxiang000000/article/details/51672725

https://www.cnblogs.com/shadowland/p/5872257.html

算法介紹(基於DFS算法)

了解tarjan算法之前你需要知道:強連通,強連通圖,強連通分量。

通:如果兩個頂點可以相互通達,則稱兩個頂點強連通(strongly connected)。

在一個有向圖G裏,設兩個點a和b, 發現由a有一條路可以走到b,由b又有一條 路可以走到a,我們就叫這兩個頂點(a,b)強連通。(註意間接連接也可以)

強連通圖:如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖。

強連通分量:在一個有向圖G中,有一個子圖G2,這個子圖G2每2個點都滿足強連通,我們就 把這個子圖G2叫做強連通分量 。

我們來看一個有向圖方便理解:

技術分享圖片

標註藍色線條框框的部分就是一個強連通分量,節點3也是一個強連通分量

我們再來看一個圖(百度百科中的圖):

子圖{1,2,3,4}

為一個強連通分量,因為頂點1,2,3,4兩兩可達。{5},{6}也分別是兩個強連通分量。

技術分享圖片

Tarjan算法是基於對圖深度優先搜索的算法,每個強連通分量實際上為搜索樹中的一棵子樹。搜索時,把當前搜索樹中未處理的節點加入到一個堆棧,回溯時可以判斷棧頂到棧中的節點是否為一個強連通分量。

Tarjan算法中,有如下定義。

(註意,下面的定義如看不明白沒關系,多看後面的模擬就明白了)

DFN[u]數組 : 在DFS中該節點被搜索的次序編號,每個點的次序編號都不一樣。

通俗地解釋DFN[u]: 意思就是在DFS的過程中,當前的這個節點是第幾個被遍歷到的點

LOW[u]數組 : u或u的子樹能夠追溯到的最早的棧中節點的次序號

通俗地解釋LOW[u]: 就是在DFS的過程中,如果當前節點是極大強聯通子圖的話,他的根節點的標號就是對應的LOW值:

DFN[ u ]==LOW[ u ]時,u或u的子樹可以構成一個強連通分量。

通俗地解釋:DFN[ u ]==LOW[ u ]時,u為根的搜索子樹上所有節點是一個強連通分量。

回溯條件: DFS遇到的節點在已在棧中或者DFS遇到無出度的節點時就回溯。

回溯時需要維護LOW[u]的值。

如果下一個要遍歷的節點在棧中,那麽就要把當前節點的LOW[u]值設置成下一個節點(在棧中)的DFN[v]值。如:LOW[u]=DFN[v] 或者LOW[u]= min(LOW[u], DFN[v])

如果還需要接著回溯,那麽接著維護LOW[u]=min(LOW[u],LOW[v])

(即使v搜過了也要進行這步操作,但是v一定要在棧內才行)

u代表當前節點,v代表其能到達的節點。在進行一層深搜之後回溯的時候,維護LOW[u]的值。如果我們發現了某個節點回溯之後維護的LOW[u]值還是==DFN[u]的值,那麽這個節點無疑就是一個關鍵節點:

算法演示:1為Tarjan 算法的起始點,如圖:前面不明白沒關系,重點從這裏開始看

技術分享圖片

假如從1號節點開始遍歷,開始dfs,並不斷設置當前節點的DFN值和LOW值,並把當前這個節點壓入棧中,那麽第一次在節點6處停止,因為6沒有出度。

那麽此時的DFN和LOW值分別為:

1開始: DFN[1]=LOW[1]= ++index ----1

入棧 1

1進入3: DFN[3]=LOW[3]= ++index ----2

入棧 1 3

3進入5: DFN[5]=LOW[5]= ++index ----3

入棧 1 3 5

5進入6: DFN[6]=LOW[6]= ++index ----4

入棧 1 3 5 6

可以用下圖來表示:

技術分享圖片

因為節點6無出度,於是判斷 DFN[6]==LOW[6],把6出棧(pop)。

{6}是一個強連通分量。

目前棧的節點有: 1 3 5 見下圖:

技術分享圖片

之後回溯到節點5,節點6被訪問過了並出棧(pop)了,所以它也沒有能訪問的邊了,

那麽 DFN[5]==LOW[5],{5} 也是一個強連通分量,彈出5

目前棧的節點有: 1 3

返回節點3,繼續搜索到節點4,節點4是新節點,設DFN[4]=LOW[4]=5並把4加入堆棧。

DFN[4]=LOW[4]= ++index -----5

入棧 1 3 4 見下圖:

技術分享圖片

繼續節點4往下找,找到了節點1 。

因為1號節點還在棧中,那麽就代表著棧中的現有的所有元素構成了一個強連通圖

(仔細想想、、兜了一圈又回到起點1)

回溯到節點4,更新 LOW[4]的值: LOW[4]= min(LOW[4], DFN[1]) 值更新為1

再接著訪問4的下一個節點6,節點6 被訪問過並POP了,就不用管它了。

再回溯到節點3,更新 LOW[3]的值: LOW[3]= min(LOW[3], LOW[4]) 值更新為1

3號節點也沒有能訪問的下一個節點了。圖如下:

技術分享圖片

再回溯到節點1,更新 LOW[1]的值: LOW[1]= min(LOW[1], LOW[3]) 值還是為1

節點1還有邊沒有走過。發現節點2,訪問節點2,節點2是新節點,放入

DFN[2]=LOW[2]=++index ----6

入棧 1 3 4 2

由節點2,走到4,發現4被訪問過了,4還在棧裏,

回溯到節點2 更新LOW[2] = min(LOW[2], DFN[4]) LOW[2]=5

節點2也沒有可訪問的下一個節點了。

再回溯到節點1 更新LOW[1] = min(LOW[1], LOW[2]) LOW[1]=1

這時我們發現LOW[1]==DFN[1] 說明以1為 根節點 的強連通分量已經找完了。

將棧中1,3,4,2全部節點都出棧{1,3,4,2} 是強連通分量。圖如下

技術分享圖片

至此,算法結束。經過該算法,求出了圖中全部的三個強連通分量{1,3,4,2}, {5}, {6}

可以發現,運行Tarjan算法的過程中,每個頂點都被訪問了一次,且只進出了一次堆棧,每條邊也只被訪問了一次,所以該算法的時間復雜度為O(N+M)

實戰:(後面有Tarjan算法的偽代碼及模板,可以參考)

P1726 上白澤慧音 https://www.luogu.org/problemnew/show/1726

P2661 信息傳遞 https://www.luogu.org/problemnew/show/2661

P3379 LCA Tarjan算法 https://www.luogu.org/problemnew/show/3379

P1262 間諜網絡 (提示:可用Tanjan縮點) https://www.luogu.org/problemnew/show/1262

P3387 【模板】縮點 https://www.luogu.org/problemnew/show/3387

以下4道題是北京大學的是英文題,如不明白意思可看下面的翻譯:

題解 http://blog.csdn.net/u012469987/article/details/51292558#poj-1236-network-of-schools

http://poj.org/problem?id=2186 http://poj.org/problem?id=1236

http://poj.org/problem?id=1904 http://poj.org/problem?id=1330

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

Tarjan算法偽代碼參考:

//註意,v指的是u能達到的下一個節點
tarjan(u){
  DFN[u]=Low[u]=++Index   // 為節點u設定次序編號和Low初值
  Stack.push(u)           // 將節點u壓入棧中
  for each (u, v) in E     // 枚舉每一條邊
    if (v is not visted) // 如果節點v未被訪問過
        tarjan(v) // 繼續向下找
        Low[u] = min(Low[u], Low[v])
    else if (v in S) // 如果節點u還在棧內
        Low[u] = min(Low[u], DFN[v])
  if (DFN[u] == Low[u]) // 如果節點u是強連通分量的根
  repeat v = S.pop  // 將v退棧,為該強連通分量中一個頂點
  print  v
  until (u== v)
}

Tanjan算法模板:

void Tarjan ( int x )
 {
  dfn[ x ] = ++dfs_num ;
  low[ x ] = dfs_num ;
  vis [ x ] = true ; //是否在棧中
  stack [ ++top ] = x ;
  for ( int i=head[ x ] ; i!=0 ; i=e[i].next )
      {
          int temp = e[ i ].to ;
          if ( !dfn[ temp ] )
           {
             Tarjan ( temp ) ;
              low[ x ] = gmin ( low[ x ] , low[ temp ] ) ;
           }
            else if ( vis[ temp ])low[ x ] = gmin ( low[ x ] , dfn[ temp ] ) ;
      }
      if ( dfn[ x ]==low[ x ] )            //構成強連通分量
      {
            vis[ x ] = false ;
             color[ x ] = ++col_num ;    //染色
             while ( stack[ top ] != x )  //清空
             {
              color [stack[ top ]] = col_num ;
             vis [ stack[ top-- ] ] = false ;
             }
                 top -- ;
       }
}

Tanjan算法另一個模板:

#define M 5010       //題目中可能的最大點數
int STACK[M],top=0;  //Tarjan算法中的棧
bool InStack[M]; //檢查是否在棧中
int DFN[M];    //深度優先搜索訪問次序
 
int Low[M];  //能追溯到的最早的次序
int ComponentNumber=0;  //有向圖強連通分量個數
int Index=0;  //索引號
vector<int> Edge[M];  //鄰接表表示
vector<int> Component[M];  //獲得強連通分量結果
int InComponent[M];  //記錄每個點在第幾號強連通分量裏
int ComponentDegree[M]; //記錄每個強連通分量的度
 
void Tarjan(int i)
{
    int j;
    DFN[i]=Low[i]=Index++;
    InStack[i]=true;STACK[++top]=i;
    for (int e=0;e<Edge[i].size();e++)
    {
        j=Edge[i][e];
        if (DFN[j]==-1)
        {
            Tarjan(j);
            Low[i]=min(Low[i],Low[j]);
        }
        else
            if (InStack[j]) Low[i]=min(Low[i],DFN[j]);
    }
    if (DFN[i]==Low[i])
    {
        ComponentNumber++;
        do{
            j=STACK[top--];
            InStack[j]=false;
            Component[ComponentNumber].
            push_back(j);
            InComponent[j]=ComponentNumber;
        }
        while (j!=i);
    }
}

Tarjan算法裸代碼:

輸入:

一個圖有向圖。

輸出:

它每個強連通分量。

這個圖就是剛才講的那個圖。一模一樣。

input:

6 8

1 3

1 2

2 4

3 4

3 5

4 6

4 1

5 6

output:

6

5

3 4 2 1

#include<cstdio>
 #include<algorithm>
 #include<string.h>
 using namespace std;
 struct node
{
     int v,next;
 }edge[1001];
 int DFN[1001],LOW[1001];
 int stack[1001],heads[1001],visit[1001],cnt,tot,index;
void add(int x,int y)
{
     edge[++cnt].next=heads[x];
     edge[cnt].v = y;
     heads[x]=cnt;
    return ;
 }
 void tarjan(int x)  //代表第幾個點在處理。遞歸的是點。
 {
     DFN[x]=LOW[x]=++tot; //新進點的初始化。
     stack[++index]=x; //進棧
     visit[x]=1;  //表示在棧裏
    for(int i=heads[x];i!=-1;i=edge[i].next)
     {
         if(!DFN[edge[i].v]) //如果沒訪問過
{   
            tarjan(edge[i].v);  //往下進行延伸,開始遞歸
             LOW[x]=min(LOW[x],LOW[edge[i].v]);//遞歸出來,比較誰是誰的兒子/父親,就是樹的對應關系,涉及到強連通分量子樹最小根的事情。
           }
        else if(visit[edge[i].v ])//如果訪問過,並且還在棧裏
{  
             LOW[x]=min(LOW[x],DFN[edge[i].v]);//比較誰是誰的兒子/父親。就是鏈接對應關系
          }
     }
     if(LOW[x]==DFN[x]) //發現是整個強連通分量子樹裏的最小根。
    {
         do{
            printf("%d ",stack[index]);
             visit[stack[index]]=0;
             index--;
         }while(x!=stack[index+1]);//出棧,並且輸出。
         printf("\n");
     }
     return ;
 }
 int main()
 {
     memset(heads,-1,sizeof(heads));
     int n,m;
     scanf("%d%d",&n,&m);
    int x,y;
     for(int i=1;i<=m;i++)
     {
         scanf("%d%d",&x,&y);
        add(x,y);
     }
    for(int i=1;i<=n;i++)
         if(!DFN[i])  tarjan(i);//當這個點沒有訪問過,就從此點開始。防止圖沒走完
    return 0;
 }

筆記:Tarjan算法 求解有向圖強連通分量的線性時間的算法