1. 程式人生 > 實用技巧 >Tarjan演算法 雙連通分量

Tarjan演算法 雙連通分量

一、邊雙連通分量

邊雙連通分量

邊雙連通圖:若一個無向圖中的去掉任意一條邊都不會改變此圖的連通性,即不存在橋,則稱作邊雙連通圖。

邊雙連通分量:無向圖中,刪除任意邊後仍然能連通的塊。簡記為“e-DCC”。(無向連通圖的極大邊雙連通分量)

定理:一張無向連通圖是“邊雙連通圖”,當且僅當任意一條邊都包含在至少一個簡單環中。

性質:橋把整張圖拆成了若干個 e-DCC,並且橋不在任意一個 e-DCC。

Tarjan演算法求邊雙連通分量

求出所有的橋以後,把橋邊刪除,原圖變成了多個連通塊,則每個連通塊就是一個邊雙連通分量。先用 Tarjan 演算法標記處所有的橋邊,再對整個無向圖 DFS 一遍(遍歷的過程中不訪問橋邊),劃分出每個連通塊。

模板題連結參考程式碼如下:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5,M=3e5+5;
int n,m,x,y,cnt=1,hd[N],to[M<<1],nxt[M<<1],dfn[N],low[N],num,c[N],dcc;
bool g[M<<1];
void add(int x,int y){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
}
void tarjan(int
x,int fa){ dfn[x]=low[x]=++num; for(int i=hd[x];i;i=nxt[i]){ int y=to[i]; if(!dfn[y]){ tarjan(y,i); low[x]=min(low[x],low[y]); if(low[y]>dfn[x]) g[i]=g[i^1]=1; } else if(i!=(fa^1)) low[x]=min(low[x],dfn[y]); } } void
dfs(int x){ c[x]=dcc; for(int i=hd[x];i;i=nxt[i]){ int y=to[i]; if(c[y]||g[i]) continue; dfs(y); } } signed main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=m;i++){ scanf("%lld%lld",&x,&y); add(x,y),add(y,x); } for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0); for(int i=1;i<=n;i++) if(!c[i]) ++dcc,dfs(i); printf("%lld\n",dcc); return 0; }

dcc 為 e-DCC 的數量,c[x] 為節點 x 所屬的“邊雙連通分量”的編號。

邊雙連通分量縮點

將所有的邊雙連通分量都縮成一個點,把邊 (x,y) 看作連線編號 c[x] 和 c[y] 的邊雙連通分量對應結點的無向邊,則原圖會變成一棵樹(若原來的無向圖不連通,則產生森林)。

int cnt2=1,hd2[N],to2[N<<1],nxt2[N<<1]; 
void add2(int x,int y){
    to2[++cnt2]=y,nxt2[cnt2]=hd2[x],hd2[x]=cnt2;
}
/*.......*/
int main(){
    /*.......*/
    for(int i=2;i<=cnt;i++){
        int x=to[i^1],y=to[i];
        if(c[x]!=c[y]) add2(c[x],c[y]);
    }
    //dcc 為縮點後森林的點數,cnt2/2 為縮點後森林的邊數 
    for(int i=2;i<cnt2;i+=2)
        printf("%lld %lld\n",to2[i^1],to2[i]); 
} 

有橋圖加邊變成邊雙連通圖

一個有橋的連通圖,通過加邊變成邊雙連通圖:求出所有的橋以後,把橋邊刪除,原圖變成了多個連通塊,則每個連通塊就是一個邊雙連通分量。把每個雙連通分量都縮成一個點,得到一棵樹。統計出樹中度為 1 的結點的個數,即葉節點的個數,記為 leaf。則至少在樹上新增 (leaf+1)/2 條邊。

結論:當葉子數為 1 時,將一個有橋圖通過加邊變成邊雙連通圖至少要新增的邊數為0;否則為 (葉子數+1)/2。

[USACO06JAN]Redundant Paths G

要求任意兩點間至少有兩條沒有公共邊的路,也就是說所要求的圖是一個邊雙連通圖。總結為,給定一個圖,求需要新增幾條邊使其變成邊雙連通圖。根據上面提到的,將一個有橋圖通過加邊變成邊雙連通圖,至少要加 (leaf+1)/2 條邊。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e3+5,M=1e4+5;
int n,m,x,y,cnt=1,hd[N],to[M<<1],nxt[M<<1],dfn[N],low[N],num,dcc,c[N],du[N],leaf;
bool g[M<<1];
void add(int x,int y){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
}
void tarjan(int x,int fa){
    dfn[x]=low[x]=++num;
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(!dfn[y]){
            tarjan(y,i);
            low[x]=min(low[x],low[y]);
            if(low[y]>dfn[x]) g[i]=g[i^1]=1;
        }
        else if(i!=(fa^1)) low[x]=min(low[x],dfn[y]);
    }
}
void dfs(int x){
    c[x]=dcc;
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(c[y]||g[i]) continue;
        dfs(y);
    }
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%lld%lld",&x,&y);
        add(x,y),add(y,x);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i]) tarjan(i,0);
    for(int i=1;i<=n;i++)
        if(!c[i]) ++dcc,dfs(i);
    for(int i=2;i<cnt;i+=2){
        int x=to[i^1],y=to[i];
        if(c[x]!=c[y]) du[c[x]]++,du[c[y]]++;
    }
    for(int i=1;i<=dcc;i++)
        if(du[i]==1) leaf++;
    printf("%lld\n",(leaf+1)/2);
    return 0;
}

二、點雙連通分量

點雙連通分量

點雙連通:若一個無向圖中的去掉任意一個節點都不會改變此圖的連通性,即不存在割點,則稱作點雙連通圖。

點雙連通分量:無向圖中,刪除任意點後仍然能連通的塊。簡記為“v-DCC”。(無向圖的極大點雙連通子圖)

定理:一張無向連通圖是“點雙連通圖”,當且僅當滿足下列兩個條件之一:

  1. 圖的頂點數不超過2
  2. 圖中任意兩點都同時包含在至少一個簡單環中。(簡單環:不自交的環)

性質:

  • v-DCC 中沒有割點。
  • 若 v-DCC 間有公共點,則公共點為原圖的割點。
  • 對於圖G,割點可能同時屬於多個 v-DCC,其它點只可能屬於一個 v-DCC。
  • 割點將整張圖分成若干個點 v-DCC。

Tarjan演算法求點雙連通分量

若某個節點為孤立點,則它自己單獨構成一個 v-DCC。除了孤立點之外,點雙連通分量的大小至少為 2。根據 v-DCC 定義中的“極大”性,雖然橋不屬於任何 e-DCC,但是割點可能屬於多個 v-DCC。

對於點雙連通分量,實際上在求割點的過程中就能順便求出每個點雙連通分量。建立一個棧,儲存當前雙連通分量,在搜尋圖時,每找到一條樹枝邊或後向邊(非橫叉邊),就把這條邊加入棧中。如果遇到滿足 dfn[x]≤low[y],說明 x 是一個割點,同時把邊從棧頂一個個取出,直到遇到邊 (x,y) 為止。取出的這些邊與其相連的點,組成一個 v-DCC。對於兩個 v-DCC,最多隻有一個公共點即割點。

模板題連結參考程式碼如下:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5,M=3e5+5;
int n,m,x,y,cnt=1,hd[N],to[M<<1],nxt[M<<1],dfn[N],low[N],num,root,top,tot,st[N];
bool g[N];
vector<int>dcc[N];
void add(int x,int y){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
}
void tarjan(int x){
    dfn[x]=low[x]=++num,st[++top]=x;
    if(x==root&&!hd[x]) return (void)(dcc[++tot].push_back(x));    //孤立點 
    int flag=0;
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(!dfn[y]){
            tarjan(y),low[x]=min(low[x],low[y]);
            if(low[y]>=dfn[x]){
                flag++;
                if(x!=root||flag>1) g[x]=1;
                dcc[++tot].push_back(st[top]);
                while(st[top]!=y) dcc[tot].push_back(st[--top]);
                --top,dcc[tot].push_back(x);
            }
        }
        else low[x]=min(low[x],dfn[y]);
    }
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%lld%lld",&x,&y);
        if(x==y) continue;
        add(x,y),add(y,x);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i]) root=i,tarjan(i);
    for(int i=1;i<=tot;i++)
        for(int j=0;j<dcc[i].size();j++)
            printf("%lld%c",dcc[i][j],j==dcc[i].size()-1?'\n':' ');
    return 0;
}

在求出割點的同時,計算出 vector 陣列 dcc,dcc[i] 儲存編號為 i 的 v-DCC 中的所有節點。

點雙連通分量縮點

因為一個割點可能屬於多個 v-DCC,所以 v-DCC 的縮點比 e-DCC 要複雜一些。設圖中共有 p 個割點和 t 個 v-DCC。我們建立一張包含 p+t 個節點的新圖,把每個 v-DCC 和每個割點都作為新圖中的節點,並在每個割點與包含它的所有 v-DCC 之間兩邊。容易發現,這張新圖其實是一棵樹(或森林)。

以下程式碼建立在 Tarjan 求個點和 v=DCC 的程式碼 main 函式的基礎上,對 v-DCC 縮點,構成一棵新的樹(或森林),儲存在另一個鄰接表中。

int cnt2=1,hd2[N],to2[N<<1],nxt2[N<<1]; 
void add2(int x,int y){
    to2[++cnt2]=y,nxt2[cnt2]=hd2[x],hd2[x]=cnt2;
}
/*.......*/
int main(){
    /*.......*/
    //給每個割點一個新的編號(編號從 tot+1 開始)
    num=tot;
    for(int i=1;i<=n;i++)
        if(g[i]) k[i]=++num;
    //建新圖,從每個 v-DCC 到它包含的所有割點連邊
    for(int i=1;i<=tot;i++)
        for(int j=0;j<dcc[i].size();j++){
            int x=dcc[i][j];
            if(g[x]) add2(i,k[x]),add2(k[x],i);
            else c[x]=i;    //除割點外,其他點僅屬於 1 個 v-DCC 
        } 
    //縮點之後的森林,點數為 num,邊數為 cnt2/2 
    //編號 1~tot 的為原圖的 v-DCC,編號 >tot 的為原圖割點
     for(int i=2;i<cnt2;i+=2)
         printf("%lld %lld\n",to2[i^1],to2[i]);
     return 0;
}