Tarjan演算法三大應用之強連通分量

programmy發表於2017-02-23

Tarjan是一個對圖的分析的強有力的演算法,主要應用有:有向圖的強連通分量、無向圖的割點橋與雙連通分量、LCA(最近公共祖先)

基本概念

下面主要介紹tarjan演算法在強連通分量中的應用。

首先我們需要知道強連通是有向圖特有的概念,如果一個有向圖中任意兩點之間都是相互可達的那麼稱這個圖為強連通圖。一個圖的極大連通子圖稱為改圖的強連通分量。

Tarjan演算法求解強連通分量

通過Tarjan演算法可以得到每個點屬於哪個連通分量。

我們可以先初步把Tarjan演算法看成是一個對圖進行深搜並結合棧對節點進行處理的演算法。

該演算法涉及到三個值:

  • dfn[i]:

    dfn[i]:
    dfn[i]表示圖中的節點i在搜尋過程中的(訪問)次序號,是第幾個訪問到的,也叫做時間戳,每個節點的時間戳都是不一樣的

  • low[i]:

    low[i]:
    low[i]表示第i個節點的子樹能夠追溯到的最早的棧中節點的次序號

  • vis[i]:

    vis[i]:
    vis[i]值為1表示i在棧中,為0表示不在棧中。

    我們先大致瞭解了tarjan演算法是做什麼的以及裡面的符號約定,不太懂也沒關係,現在我們換一種方式來觀察這個演算法到底是做什麼的。

    我們始終應該明確的一點是我們要求的是每個點屬於哪個連通分量,其實上面的low[i]的值就表示的是點i和哪個點(這個點也被稱作根,但不一定是極小的根)是屬於同一個連通分量的。

    因為tarjan演算法本質上是一個DFS的過程,這個演算法可以看成是在一顆“搜尋樹上進行”(這裡說樹也不太準確,但不妨礙我們理解)

    我們以下圖為例:

這裡寫圖片描述

轉化成搜尋樹的形式就是

這裡寫圖片描述

可以看出整個搜尋的過程是1->2->3->5,每搜到一個就進棧(這個棧不是指用於DFS的棧,而是另外開的一個棧,用來存放被搜到的節點中還沒確定連通分量的節點)

搜到5之後繼續搜發現又搜到了2,這個時候我們發現成環了。這個環上時間戳最小的是節點2,那麼當前這個連通分量(環)的根就是dfn[2]。

回溯之後2、3、5節點的low值都為dfn[2]了,

然後繼續從2搜,發現又搜到1,而1的時間戳更小,所以以2為根連通分量將合併到以1為根的連通分量。

回溯後繼續從1搜搜到4,發現4沒有子節點也就是(low[4]==dfn[4]),4就是根節點,這個連通分量只有一個節點,所以以節點4為集合的一個極大連通分量。

再回溯,發現從1出發也沒有點可搜了,此時low[1]==dfn[1]說明此時1作為根節點的連通分量是個極大連通分量。

整個搜尋就完成了。

通過這個例子我們可以總結出演算法在搜尋時的規則:

從點u出發

1.如果從u出發沒有點可以搜了並且low[u]==dfn[u]:那麼說明我們已經遍歷了這個點所屬的連通分量並且這個點就是該連通分量的根(我們的演算法也保證了在搜尋過程中該連通分量上的每個點的low值都更新為該連通分量的根),由於該連通分量已經確定了,所以我們可以把以u點為根的連通分量從圖中刪掉。

2.如果搜到的下一個節點v未被訪問過(可以由dfn來判斷):那麼就把它進棧並從它出發進行深搜。

3.如果搜到的下一個點v被訪問過並且在棧中(也就是vis[i]=1,那些被訪問過但已經出棧的節點所在的連通分量已經確定了,所以可以忽略這些點):那麼說明從v到u這條鏈上的點都屬於一個連通分量(因為把(u,v)練下去就會成環),這個時候就應該更新每個節點的low值為這個連通分量中時間戳最小的點的low值,在回溯的過程中也不斷將這個最小的low值傳遞。

這些規則就是tarjan演算法的基本框架了,具體的演算法細節請見程式碼。

模板

const int MAXN=105;
int n;
int DFN[MAXN];
int LOW[MAXN];
int vis[MAXN];
int belong[MAXN];//belong[i]表示i屬於縮點後的哪個節點
int cnt;
int tot;
struct Edge
{
      int v;
      int next;
}edge[MAXN*MAXN];
int edgecount;
int head[MAXN];
void Init()
{
      edgecount=0;
      memset(head,-1,sizeof(head));
}
void Add_edge(int u,int v)
{
      edge[++edgecount].v=v;
      edge[edgecount].next=head[u];
      head[u]=edgecount;
}
stack<int > St;
void Tarjan(int u)//從節點x開始搜尋
{
     DFN[u]=LOW[u]=++tot;
     vis[u]=1;//為1表示在佇列裡面
     St.push(u);
     for(int k=head[u];k!=-1;k=edge[k].next)
     {

           int v=edge[k].v;
           if(!DFN[v])//還未訪問過
           {
                 Tarjan(v);
                 LOW[u]=min(LOW[u],LOW[v]);
           }
           else if(vis[v])//被訪問過,還在佇列裡
           {
                 LOW[u]=min(LOW[u],DFN[v]);
           }
     }
     if(LOW[u]==DFN[u])
     {
           int x;
           ++cnt;
           while(1)
           {
                 x=St.top();
                 St.pop();
                 vis[x]=0;
                 belong[x]=cnt;
                 if(x==u)break;
           }
     }
}
void Solve()
{
    tot=0;
    cnt=0;//縮點後的點數
    memset(DFN,0,sizeof(DFN));
    memset(LOW,0,sizeof(LOW));
    memset(vis,0,sizeof(vis));
    while(!St.empty()) St.pop();
    for(int i=1;i<=n;i++)
    {
          if(DFN[i]==0)Tarjan(i);
    }
}

應用

POJ 1236(tarjan 強連通分量 縮點)

相關文章