1. 程式人生 > >Tarjan三大算法之雙連通分量(雙連通分量) (轉載)

Tarjan三大算法之雙連通分量(雙連通分量) (轉載)

進行 ack clear 例題 min 路徑 ace 相關 重復

定義:
對於一個連通圖,如果任意兩點至少存在兩條點不重復路徑,則稱這個圖為點雙連通的(簡稱雙連通);如果任意兩點至少存在兩條邊不重復路徑,則稱該圖為邊雙連通的。點雙連通圖的定義等價於任意兩條邊都同在一個簡單環中,而邊雙連通圖的定義等價於任意一條邊至少在一個簡單環中。對一個無向圖,點雙連通的極大子圖稱為點雙連通分量(簡稱雙連通分量),邊雙連通的極大子圖稱為邊雙連通分量。這篇博客就是總結一下求解無向圖點雙連通分量與邊雙連通分量的方法。

算法:
求解點雙連通分量與邊雙連通分量其實和求解割點與橋密切相關。不同雙連通分量最多只有一個公共點,即某一個割頂,任意一個割頂都是至少兩個點雙連通的公共點。不同邊雙連通分量沒有公共點,而橋不在任何一個邊雙連通分量中,點雙連通分量一定是一個邊雙連通分量。
下面首先介紹點雙連通分量的Tarjan算法


在之前的博客中,我們已經知道如何求解割頂了,很容易可以發現,當我們找到割頂的時候,就已經完成了一次對某個極大點雙連通子圖的訪問,那麽我們如果在進行DFS的過程中將遍歷過的點保存起來,是不是就可以得到點雙連通分量了?為了實現算法,我們可以在求解割頂的過程中用一個棧保存遍歷過的(註意不是點!因為不同的雙連通分量存在公共點即割頂),之後每當找到一個點雙連通分量,即子結點v與父節點u滿足關系low[v]>=dfn[u],我們就將棧裏的東西拿出來直到遇到當前邊。
這裏註意放入棧中的不是點,而是邊,這是因為點雙連通分量是存在重復點的,如果我們放入棧中的是點,那麽對於某些點雙連通分量,就會少掉一些點(這些點都是割頂)。
代碼:

技術分享
struct Edge{
    int u,v;
    Edge(int u=0,int v=0):u(u),v(v){}
}e[maxm];
int n,m,stamp,dfn[maxn],low[maxn],iscut[maxn],bccno[maxn];
int scnt,stack[maxm],bcc_cnt;
vector<int> vec[maxn],bcc[maxn];

void tarjan(int index,int fa)
{
    int child=0,tmp;
    dfn[index]=low[index]=++stamp;
    for(int
i=0;i<vec[index].size();i++) { tmp=e[vec[index][i]].v; if(!dfn[tmp]) { stack[++scnt]=vec[index][i],child++; tarjan(tmp,index); low[index]=min(low[index],low[tmp]); if(low[tmp]>=dfn[index]) { iscut[index]=1; bcc[++bcc_cnt].clear(); while(1) { int num=stack[scnt--]; if(bccno[e[num].u]!=bcc_cnt) { bcc[bcc_cnt].push_back(e[num].u); bccno[e[num].u]=bcc_cnt; } if(bccno[e[num].v]!=bcc_cnt) { bcc[bcc_cnt].push_back(e[num].v); bccno[e[num].v]=bcc_cnt; } if(e[num].u==index && e[num].v==tmp) break; } } } else if(dfn[tmp]<dfn[index] && tmp!=fa) { stack[++scnt]=vec[index][i]; low[index]=min(low[index], dfn[tmp]); } } if(fa<0 && child==1) iscut[index]=0; } void find_bcc() { // 割頂的bccno值無意義 memset(dfn,0,sizeof(dfn)); memset(low,0,sizeof(low)); memset(iscut,0,sizeof(iscut)); memset(bccno,0,sizeof(bccno)); memset(bcc,0,sizeof(bcc)); stamp=scnt=bcc_cnt=0; for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,-1);
View Code

這裏需要十分註意的是,算法結束之後,每個結點會有一個編號,代表它屬於哪一個點雙連通分量,但是,割頂的編號是完全沒有意義的!這個算法靈活使用了兩個時間戳和棧,完成了點雙連通分量的發現。
例題:UVALIVE 5135

之後介紹邊雙連通分量的求解算法:
邊雙連通分量的求解非常簡單,因為邊雙連通分量之間沒有公共邊,而且橋不在任意一個邊雙連通分量中,所以算法十分簡單,即先一次DFS找到所有橋,再一次DFS(排除了橋)找到邊雙連通分量。
PS:當然可以用一次DFS實現。
代碼:

技術分享
struct Edge{
    int u,v;
    Edge(int u=0,int v=0):u(u),v(v){}
}e[maxm];
int n,m,stamp,dfn[maxn],low[maxn],bccno[maxn],bcc_cnt;
vector<int> vec[maxn],bcc[maxn];
bool g[maxn][maxn],isbridge[maxm];

void tarjan(int index,int fa)
{
    int tmp;
    dfn[index]=low[index]=++stamp;
    for(int i=0;i<vec[index].size();i++)
    {
        tmp=e[vec[index][i]].v;
        if(!dfn[tmp])
        {
            tarjan(tmp,index);
            low[index]=min(low[index],low[tmp]);
            if(low[tmp]>dfn[index])
                isbridge[vec[index][i]]=isbridge[vec[index][i]^1]=1;
        }
        else if(dfn[tmp]<dfn[index] && tmp!=fa)
        {
            low[index]=min(low[index], dfn[tmp]);
        }
    }
}

void dfs(int index)
{
    dfn[index]=1;
    bccno[index]=bcc_cnt;
    for(int i=0;i<vec[index].size();i++)
    {
        int tmp=vec[index][i];
        if(isbridge[tmp])
            continue;
        if(!dfn[e[tmp].v])
        {
            dfs(e[tmp].v);
        }
    }
}

void find_ebcc(){
    bcc_cnt=stamp=0;
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    memset(isbridge,0,sizeof(isbridge));
    memset(bccno,0,sizeof(bccno));
    memset(bcc,0,sizeof(bcc));
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i, -1);
    memset(dfn,0,sizeof(dfn));
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])
        {
            bcc_cnt++;
            dfs(i);
        }
    }               
}
View Code

POJ 3352

所謂雙連通與強連通,最大的差別,也是最本質的差別就是前者適用於無向圖中,而後者適用於有向圖

Tarjan三大算法之雙連通分量(雙連通分量) (轉載)