強聯通分量及縮點法

LR0發表於2018-06-12

概念

1.連通性:如果在圖中存在一條路徑將頂點u,v連線在了一起,則稱u,v是連通的。

2.連通分量:無向圖G的極大連通子圖稱為G的連通分量( Connected Component),就是再加入一個新點,這個新點不能與分量中所有點連通

3.強連通分量:有向圖中, u可達v不一定意味著v可達u. 相互可達則屬於同一個強連通分量(Strongly Connected Component)

4.連通圖:如果圖中所有頂點都是互相連通的,則稱這個圖是一個連通圖

強連通分量及縮點法

我們可以將每個強連通分量看作一個內外隔絕的包裹,忽略包裹內部的冗餘邊,並將這個包裹同外部點的相連的邊保留,將其打包壓縮成一個新的點儲存下來,這就是縮點法。


如圖,s1,s2,s3就是圖的三個強連通分量,可以把他們壓縮成3個新點,壓縮後的新點形成的一定是個有向無環圖,如果新點成環的話就意味著環上的任意兩點相互連通,意味著兩個強連通分量中的點相互連通,則這兩點同屬於一個強連通分量,矛盾

所以縮點法形成的新圖一定是有向無環圖,這個性質有時對解決問題會有極大的幫助。

求連通分量的具體演算法主要有三種,Kosaraju,Gabow和Tarjan演算法,下面對這三種演算法逐一進行介紹。

Tarjan演算法

由於強連通分量中的點相互連通,所以如果用dfs遍歷到這個分量時,一定會回溯到已經遍歷過的同屬於這個分量的點


如圖所示,圖的一個強連通分量會在dfs時會形成以A為根節點的子樹,我們只需要找出這個子樹,並能夠取出這個子樹,也即利用Targan演算法,Targan演算法基於DFS和棧來實現,每次遍歷到一個點時就把該點壓棧。

首先建立兩個陣列DFN[] LOW[], DFN[]用來記錄點被遍歷到的時候的時間,(會再定義一個全域性變數做計時器),作用在於區分點,以及識別根,因為,當DFS走到強連通分量中的第一個點時,這個點的DFN[]一定是最小的,如圖中的A。

LOW[]記錄每個點能夠回溯到的點的最小的DFN值,如B能夠回溯到A,他的LOW實際就是A的DFN

LOW值一定小於DFN

一旦點的DFN不等於其LOW時,意味著他可以回溯到更早的點,所以這個點一定不是根節點。

當一個點的DFN==LOW時,這個點就是根,就將棧中該點及之上的所有點出棧,他們同屬於一個強連通分量。


vector<int> G[10010];
stack<int> s;
int low[10010];
int dfn[10010];
int time = 0;
int scc[10010];
int sccnum = 0;
int visit[10010];
int numinscc[10010];
int outdegree[10010];
void Targan(int u)
{
    low[u] = dfn[u] = ++time;
    visit[u] = 1;
    s.push(u);

    for(int i=0;i<G[u].size();i++)
    {
        int v = G[u][i];
        if(visit[v] == 0)
        {
            Targan(v);
            low[u] = min(low[u],low[v]);
        }
        else if(visit[v] == 1&&scc[v] == 0)
            low[u] = min(low[u],dfn[v]);
    }


    int m;
    if(dfn[u] == low[u])
    {
        sccnum++;
        do
        {
            m = s.top();
            s.pop();
            scc[m] = sccnum;
            numinscc[sccnum]++;
        }while(m!=u);
    }


}


得到強連通分量之後可以遍歷每條邊,如果邊的兩頂點不在同一個強連通分量,則可以把這個縮點的出度加1

Gabow演算法

Gabow演算法的原理和Targan演算法類似,只是Gabow演算法將LOW陣列用另一個棧代替,即用雙棧實現演算法

每次遍歷到新點時,就把該點同時壓入兩個棧,因為強連通分量是由一個個環組成的,所以每當回溯到棧中的點導致成環時

就把棧2中該環內根節點以上的點彈出,只保留根節點,當從某點出發全部dfs完了之後,棧二的頂點就是該點,那麼這個點就是強連通分量的根節點,這時棧1該點及以上的所有點就組成了強連通分量,即慢慢剝離強連通分量中的環達到定位根節點的目的

Gabow演算法也利用了陣列DFN來為節點編序。

vector<int> G[10010];
stack<int> s1;
stack<int> s2;
//int low[10010];
int dfn[10010];
int time = 0;
int scc[10010];
int sccnum = 0;
int visit[10010];
int outdegree[10010];
int numinscc[10010];


void Gabow(int u)
{
    visit[u] =1;
    dfn[u] = ++time;
    s1.push(u);
    s2.push(u);

    for(int i=0;i<G[u].size();i++)
    {
        int v = G[u][i];
        if(visit[v] == 0)
            Gabow(v);
        else if(visit[v]==1&&scc[v]==0)
        {
            while(dfn[s2.top()]>dfn[v])
                s2.pop();
        }
    }
    if(s2.top() == u)
    {
        int m;
        sccnum++;
        do
        {
            m=s1.top();
            s1.pop();
            scc[m] = sccnum;
            numinscc[sccnum]++;
        }while(m!=u);
    }
}

Kosaraju演算法

對於一個無向圖的連通分量,從連通分量的任意一個頂點開始,進行一次DFS,一定能遍歷這個連通分量的所有頂點。所以,整個圖的連通分量數應該等價於遍歷整個圖進行了幾次(最外層的)DFS。一次DFS中遍歷的所有頂點屬於同一個連通分量。
而對於有向圖,dfs遍歷到的頂點未必組成一個強連通分量。


如果從A0開始遍歷,則整個圖都能遍歷完,但是這個圖並不是一個強連通分量

但是會發現,如果我們先遍歷B中的頂點,則第一次DFS將遍歷B3、B4、B5組成的強連通分量。第二次DFS將遍歷A0、A1、A2

組成的強連通分量,這樣我們就想到一個策略就是如果能得到一個頂點遍歷的順序,滿足每次按順序遍歷一次DFS,就能遍歷出一個強連通分量就好了,好的是這樣的順序是存在的

我們把原圖反向,所有的邊反向


建立一個棧,在DFS,當頂點所有的邊都被遍歷完時,把這個頂點壓入棧中

第一種情況,先遍歷A0、A1、A2,則第一次DFS後,三點全部入棧,第二次DFS後B3、B4、B5入棧,滿足B系列的點在A系列的點上面(在棧中)

第二種情況,先遍歷B系列的點,因為壓棧操作在所有的邊被遍歷完之後,所以當B系列的點要被壓棧時,A系列的點已經遍歷完了,所以B系列的點依然在A系列的點上面。這樣從棧頂到棧頂的頂點形成的順序就是我們要的序列。

按這個順序DFS就得到了各強連通分量,這個方法對複雜情況也是成立的。即演算法分為兩步:

(1)對原圖取反,從任意一個頂點開始對反向圖進行逆後續DFS遍歷

(2)按照逆後續遍歷中棧中的頂點出棧順序,對原圖進行DFS遍歷,一次DFS遍歷中訪問的所有頂點都屬於同一強連通分量。

vector<int>G[maxn],G2[maxn];  
vector<int>S;  
int vis[maxn],sccno[maxn],scc_cnt;  
  
void dfs1(int u)  
{  
    if (vis[u]) return;  
    vis[u]=1;  
    for (int i=0;i<G[u].size();i++) dfs1(G[u][i]);  
    S.push_back(u);  
}  
  
void dfs2(int u)  
{  
    if (sccno[u]) return;  
    sccno[u]=scc_cnt;  
    for (int i=0;i<G2[u].size();i++) dfs2(G2[u][i]);  
}  
  
void find_scc(int n)  
{  
    scc_cnt=0;  
    S.clear();  
    memset(sccno,0,sizeof(sccno));  
    memset(vis,0,sizeof(vis));  
    for (int i=0;i<n;i++) dfs1(i);  
    for (int i=n-1;i>=0;i--)  
    {  
        if (!sccno[S[i]])  
        {  
            scc_cnt++;  
            dfs2(S[i]);  
        }  
    }  
}  


例題:POJ2186


考慮這樣一個例子,先求出圖中的強連通分量,然後縮點成新圖

則S3中的牛都是滿足題意的受所有牛仰慕的牛,即縮點後的新圖若只有一個出度為零的點,則這個點就是滿足題意的點

該點內的所有牛都是滿足題意的牛,若不止一個出度為0的點,則滿足提議的牛為0

下面僅附上Targan演算法程式

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <vector>
#include <stack>
using namespace std;

vector<int> G[10010];
stack<int> s;
int low[10010];
int dfn[10010];
int time = 0;
int scc[10010];
int sccnum = 0;
int visit[10010];
int numinscc[10010];
int outdegree[10010];
void Targan(int u)
{
    low[u] = dfn[u] = ++time;
    visit[u] = 1;
    s.push(u);

    for(int i=0;i<G[u].size();i++)
    {
        int v = G[u][i];
        if(visit[v] == 0)
        {
            Targan(v);
            low[u] = min(low[u],low[v]);
        }
        else if(visit[v] == 1&&scc[v] == 0)
            low[u] = min(low[u],dfn[v]);
    }


    int m;
    if(dfn[u] == low[u])
    {
        sccnum++;
        do
        {
            m = s.top();
            s.pop();
            scc[m] = sccnum;
            numinscc[sccnum]++;
            //outdegree[sccnum] += G[m].size();
        }while(m!=u);
    }


}

int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        G[u].push_back(v);
    }

    for(int i=1;i<=n;i++)
    {
        if(scc[i]==0)
            Targan(i);
    }

    for(int i =1;i<=n;i++)
    {
        for(int j=0;j<G[i].size();j++)
        {
            int v = G[i][j];
            if(scc[i]!=scc[v])
                outdegree[scc[i]]++;
        }
    }

    int nq = 0,num = 0;

    for(int i=1;i<=sccnum;i++)
    {
        if(outdegree[i]==0)
        {num++;nq = i;}
    }
    if(num==1)
        cout << numinscc[nq]<<endl;
    else
        cout << 0 <<endl;
}







相關文章