這篇介紹如何用Tarjan算法求Double Connected Component,即雙連通分量。
雙聯通分量包括點雙連通分量v-DCC和邊連通分量e-DCC。
若一張無向連通圖不存在割點,則稱它為“點雙連通圖”,不存在橋則稱為“邊雙連通圖”。
無向圖的極大點雙連通子圖就v-DCC,極大邊雙連通子圖就是e-DCC。
上一篇我們講了如何用Tarjan算法求出無向圖中的所有割點和橋。
不會求的朋友們可以去看一看上篇文章:Tarjan算法求無向圖的割點和橋
這里“極大”的定義可以理解為包含部分點的最大的雙連通子圖,即不存在比包含它且比它更大的雙連通子圖。
下面給出幾個定理:
1. 一張無向連通圖是點雙連通圖當且僅當 圖的頂點數<=2 or 圖中任意兩點都同時包含在至少一個簡單環中。
2. 一張無向連通圖是邊雙連通圖當且僅當任意一條邊都包含在至少一個簡單換中。
接下來講求法:
e-DCC的求法很簡單,通過一遍Tarjan算法找到所有的橋,把橋刪除后,無向圖會分裂成一個個連通塊。
每一個連通塊都是一個e-DCC。
具體實現就是先用Tarjan算法標記所有橋,然后對整張圖dfs一遍(不訪問橋邊),划分出所有連通塊。
一般可以用一個數組表示每個節點所在的e-DCC的編號。
代碼如下:
#include<bits/stdc++.h> #define N 100010 using namespace std; inline int read(){ int data=0,w=1;char ch=0; while(ch!='-' && (ch<'0'||ch>'9'))ch=getchar(); if(ch=='-')w=-1,ch=getchar(); while(ch>='0' && ch<='9')data=data*10+ch-'0',ch=getchar(); return data*w; } struct Edge{ int nxt,to; #define nxt(x) e[x].nxt #define to(x) e[x].to }e[N<<1]; int head[N],tot=1,n,m,cnt,dfn[N],low[N],c[N],bridge[N],dcc; //c[x]儲存x所在的e-DCC的編號,dcc存e-DCC的數量 inline void addedge(int f,int t){ nxt(++tot)=head[f];to(tot)=t;head[f]=tot; } void tarjan(int x,int in_edge){ dfn[x]=low[x]=++cnt; for(int i=head[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]) bridge[i]=bridge[i^1]=1; }else if(i!=(in_edge^1)) low[x]=min(low[x],dfn[y]); } }//Tarjan標記橋 void dfs(int x){ c[x]=dcc;//標號 for(int i=head[x];i;i=nxt(i)){ int y=to(i); if(c[y]||bridge[i])continue;//如果已經有標號了或者這條邊是橋就不訪問 dfs(y); } } int main(){ n=read();m=read(); for(int i=1;i<=m;i++){ int x=read(),y=read(); addedge(x,y);addedge(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=1;i<=n;i++) printf("%d %d\n",i,c[i]); return 0; }
接下來講v-DCC的求法。
v-DCC是一個很容易混淆的概念。
由於v-DCC定義中的“極大”,一個割點可能屬於多個v-DCC。
為了求出v-DCC,我們需要在Tarjan的過程中維護一個棧。
當一個點第一次被訪問時,我們將它入棧。而當割點判定法則成立時,無論x是否為根,都要
從棧頂不斷彈出節點直到y節點被彈出,這些被彈出的節點包括x節點一起構成一個v-DCC。
聽上去挺簡單的,實際上代碼也很好寫。
#include<bits/stdc++.h> #define N 100010 using namespace std; inline int read(){ int data=0,w=1;char ch=0; while(ch!='-' && (ch<'0'||ch>'9'))ch=getchar(); if(ch=='-')w=-1,ch=getchar(); while(ch>='0' && ch<='9')data=data*10+ch-'0',ch=getchar(); return data*w; } struct Edge{ int nxt,to; #define nxt(x) e[x].nxt #define to(x) e[x].to }e[N<<1]; int head[N],tot=1,n,m,rt,dfn[N],low[N],cnt,stk[N],top,num,cut[N]; vector<int> dcc[N]; inline void addedge(int f,int t){ nxt(++tot)=head[f];to(tot)=t;head[f]=tot; } void tarjan(int x){ dfn[x]=low[x]=++cnt; stk[++top]=x;//第一次訪問該節點,入棧 if(x==rt && head[x]==0){//判斷孤立點,直接插入vector dcc[++num].push_back(x); return; } int flag=0; for(int i=head[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!=rt||flag>1)cut[x]=1; num++;int z;//根據上面描述的做法,把所有棧中的節點插入vector do{ z=stk[top--]; dcc[num].push_back(z);//全部插入 }while(z!=y); dcc[num].push_back(x);//包括x自己 } }else low[x]=min(low[x],dfn[y]);//不在搜索樹上,繼續更新low } } int main(){ n=read();m=read(); for(int i=1;i<=m;i++){ int x=read(),y=read(); addedge(x,y);addedge(y,x); } for(int i=1;i<=n;i++) if(!dfn[i])tarjan(i); for(int i=1;i<=num;i++){ printf("%d:",i);//第i個v-DCC for(int j=0;j<dcc[i].size();j++) printf(" %d",dcc[i][j]);//第i個v-DCC中所有的點 putchar('\n'); } return 0; }
那么這一篇就講到這里了,下一篇寫一篇短博客更新e-DCC和v-DCC的縮點。