前言
被某brz逼着問,覺得很有必要好好複習一下這 些 毒瘤東西。
定義
連通 如果有向圖中的兩點 , 間同時存在 到 的路徑及 到 的路徑,則稱點 和點 是連通的(Connected)。
連通圖 對於無向圖G,如果任意兩點都是連通的,則稱無向圖G是連通圖(Connected Graph)。
連通分量 無向圖G的極大連通子圖稱爲G的連通分量(Connected Component)。
割點 在一個無向圖中,若刪去點 和點 連出的所有邊後,連通分量的數量增加,則稱點 爲割點(Cut-vertex),也叫割頂。
割邊 在一個無向圖中,若刪去一條邊後,連通分量的數量增加,則稱該邊爲割邊(Cut-edge),也叫橋(Bridge)。
點-雙連通 對於一個無向連通圖,如果其中不存在割點,則說這個圖是點-雙連通的(Point Biconnected)。
點雙連通分量 對於一張無向圖,點-雙連通的極大子圖稱爲點雙連通分量(Point Biconnected Component,BCC)或塊(Block)。
邊-雙連通 對於一個無向連通圖,如果任意兩點之間至少存在兩條邊不重複的路徑,則說這個圖是邊-雙連通的(Edge Biconnected)。
邊雙連通分量 對於一張無向圖,邊-雙連通的極大子圖稱爲邊雙連通分量(Edge Biconnected Component)。
強聯通 如果有向圖中的兩點 , 間同時存在 到 的路徑及 到 的路徑,則稱點 和點 是強連通的(Strongly Connected)。
強聯通圖 對於一張有向圖G,如果任意兩點都是強連通的,則稱有向圖G是強聯通圖(Strongly Connected Graph)。
強聯通分量 有向圖G的極大強連通子圖,稱爲強連通分量(strongly connected components)。
說明 本文中點雙聯通分量的定義可能與其他文章不符,但據我查到的所有文獻中,都認爲兩個點一條邊組成的圖是點雙聯通分量,因此這裏直接沿用更加嚴謹的定義。
基本結論/性質
- 一張 個點的簡單連通圖中至少有 條邊。
- 點-雙連通的圖中任意兩條邊都在同一個簡單環中,即除了說明中提到的一種特殊情況之外,任意兩點之間至少存在兩條點不重複的路徑。
- 邊-雙連通的圖中任意每條邊都至少在一個簡單環中,即所有的邊都不是橋。
- 除了橋不屬於任何邊-雙連通分量外,每條邊恰好屬於一個邊-雙連通分量。
- 不同的雙連通分量最多隻有一個公共點,且它一定是割頂。
- 任意割頂都是至少兩個不同雙連通分量的公共點。
- 任意非割點只屬於一個點雙聯通分量。
- 把所有橋刪除後,每個連通分量對應原圖中的一個邊-雙連通分量。
具體證明都可以用反證法弄出來,可以參考下我的另一篇文。後文中將直接運用性質+[性質編號]的形式提到這些性質和結論。
擴展結論/性質
- 將一張有向圖中的一個強聯通分量的每條邊變爲無向的,則這個強連通分量變爲一個邊雙聯通分量。
Tarjan算法
這個算法的核心思想非常精簡,考慮對於每個點維護兩個元素
- 表示到達點 之前已經到達了多少個點。
- 表示點 在不經過父親/父子邊的前提下能到達的最早的祖先的dfn值。
第二條具體是父親還是父子邊由求解的內容不同而變化。
代碼約定
- :點 的第1條出邊的編號
- :第 條邊的信息
- :從第 條邊出發點出發的下一條邊
- :第 條邊的到達點
- 表示第 條邊的反向邊的編號(無向圖雙向連邊),這意味着邊要從2開始編號,即 和 均不能用於存儲真正的邊的信息。
割邊
考慮遍歷到一個點時,定義不重複經過深度優先搜索時已經經過的邊(這意味着可以走重邊),能到達的最早的點爲 。若點 滿足 ,則說明在不經過父子邊的情況下,連父親都到不了,則說明邊 爲割邊。
注意原圖可能不連通,因此需要保證每個聯通塊都被遍歷到。
int low[maxn],dfn[maxn];
int Index,bridge;
bool vis[maxn],cut[maxn];
void dfs(int u,int from=-1){
low[u]=dfn[u]=++Index;
vis[u]=true;
for(int i=head[u];i;i=edges[i].next){
int v=edges[i].to;
if(i==from||(i^1)==from) continue;
if(!dfn[v]){
dfs(v,i);
if(low[u]>low[v])low[u]=low[v];
if(low[v]>dfn[u]){
bridge++;
cut[i]=cut[i^1]=true;
}
} else if(vis[v])
low[u]=min(low[u],dfn[v]);
}
}
void Tarjan(int n){
memset(dfn,0,sizeof dfn);
for(int i=1;i<=n;i++)
if(!dfn[i]) dfs(i);
}
割點
考慮遍歷到一個點時,定義其不重複經過深度優先搜索時已經經過的點,能到達的最早的點爲 low[u]。若點 滿足 ,則說明邊 爲割點。
需要注意的是,若一個點是根節點,即其沒有父親,且僅有一個兒子,該點不是割點。
bool cut[maxn];
int Index,low[maxn],dfn[maxn];
void dfs(int u,int fa=-1){
low[u]=dfn[u]=++Index; int child=0;
for(int i=head[u]; i; i=edges[i].next) {
int v=edges[i].v;
if(!dfn[v]){
child++;
dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
cut[u]=1;
}else if(dfn[v]<dfn[u]&&v!=fa)
low[u]=min(low[u],dfn[v]);
} if(fa<0&&child==1) cut[u]=0;
}
void Tarjan(int n){
Index=0;
memset(dfn,0,sizeof dfn);
for(int i=1;i<=n;i++)
if(!dfn[i]) dfs(i);
}
邊雙聯通分量
根據性質3,我們可以知道一個圖是邊雙聯通分量的必要條件是圖中沒有橋。爲什麼這裏不是充要條件呢?因爲這個沒有橋的圖很可能不是隻是一個邊雙聯通分量的子圖而非極大子圖。
因此,一種簡單的方法是,先求出所有的橋,然後在不經過橋的基礎上,一個點所能到達的所有點和這個點屬於同一個邊雙聯通分量。儘管這個過程需要兩次遍歷,但在很多情況下已經足夠了。此處略去代碼。
事實上,我們只需要一次遍歷,我們考慮兩次遍歷同時進行, 用一個棧保存另一次 遍歷途中經過的所有點,具體地說,在遇到一個點時,將這個點的編號加入棧中。
當我們離開一個點時,對於點 ,若其不滿足 ,則說明這個點與其父親的連邊不是橋,可以直接離開,而不必將這個點從棧中彈出。
若滿足 ,則說明點 和其父親的連邊爲橋,則說明 有 和點 屬於同一個邊雙聯通分量。
我們考慮將棧中的元素依次彈出,直到遇到第一個不滿足上述條件的點,這樣我們就完成了對一個邊雙聯通分量的標記。理解這個過程需要對棧的所有操作整體理解,代碼如下。
int low[maxn],dfn[maxn];
int Index,ecc_cnt,bridge;
int sta[maxn],top;
bool vis[maxn],cut[maxn];
int belong[maxn];
void dfs(int u,int from=-1){
low[u]=dfn[u]=++Index;
sta[top++]=u;
vis[u]=true;
for(int i=head[u];i;i=edges[i].next){
int v=edges[i].to;
if(i==from||(i^1)==from) continue;
if(!dfn[v]){
dfs(v,i);
if(low[u]>low[v]) low[u]=low[v];
if(low[v]>dfn[u]){
bridge++;
cut[i]=true;
cut[i^1]=true;
}
} else if(vis[v])
low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
ecc_cnt++; int v;
do{
v=sta[--top];
vis[v]=false;
belong[v]=ecc_cnt;
}while(v!=u);
}
}
void solve(int n){
memset(dfn,0,sizeof dfn);
memset(cut,0,sizeof cut);
memset(vis,0,sizeof vis);
Index=top=ecc_cnt=bridge=0;
for(int i=1;i<=n;i++)
if(!dfn[i]) Tarjan(1);
}
點雙連通分量
求點雙的時候就不能兩次遍歷了,求點雙聯通分量也有一次遍歷的方法,但相對邊雙聯通分量顯得更加複雜。因此在效率允許的前提下,上述方法更爲簡便。
求邊雙聯通分量時,我們直接通過一個棧來保存邊雙聯通分量中的所有點。能這麼做是因爲每個點只可能在一個邊雙聯通分量中出現,儘管所有原圖的非割點都只會在一個點雙聯通分量中出現,但原圖的割點卻必然在多個點雙聯通分量中出現,因而不能簡單地將點入棧。
由於每條邊最多隻屬於一個點雙聯通分量,我們考慮將邊入棧。
在通過一條邊時,將這條邊入棧,若對於點 和其兒子 ,有 ,則說明點 是割點,因此我們將棧內的邊不斷彈出,並記錄邊的兩個端點,這些端點和 共屬一個點雙聯通分量,直到彈出的邊爲 爲止。
注意,在經過反向邊更新low時,也需要將這條反向邊入棧。
struct Edge{
int u,v;
};stack<Edge> sta;
int dfn[maxn],low[maxn];
int Index,bcc_cnt;
bool cut[maxn];
int belong[maxn];
void dfs(int u,int fa=-1){
low[u]=dfn[u]=++Index;
int child=0;
for(int i=head[u];i;i=edges[i].next){
int v=edges[i].v;
Edge len=(Edge){u,v};
if(!dfn[v]){
sta.push(len);
child++; dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){
cut[u]=1;
bcc_cnt++;
while(true){
Edge x=sta.top(); sta.pop();
if(belong[x.u]!=bcc_cnt) belong[x.u]=bcc_cnt;
if(belong[x.v]!=bcc_cnt) belong[x.v]=bcc_cnt;
if(x.u==u&&x.v==v) break;
}
}
}else if(dfn[v]<dfn[u]&&v!=fa){
sta.push(len);
low[u]=min(low[u],dfn[v]);
}
} if(fa<0&&child==1) cut[u]=0;
}