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(intx,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]); } } voiddfs(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。
要求任意兩點間至少有兩條沒有公共邊的路,也就是說所要求的圖是一個邊雙連通圖。總結為,給定一個圖,求需要新增幾條邊使其變成邊雙連通圖。根據上面提到的,將一個有橋圖通過加邊變成邊雙連通圖,至少要加 (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”。(無向圖的極大點雙連通子圖)
定理:一張無向連通圖是“點雙連通圖”,當且僅當滿足下列兩個條件之一:
- 圖的頂點數不超過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; }