史上代碼最簡單,講解最清晰的雙連通分量
(需提前學習強連通分量)
雙連通分量的主要內容包括割點、橋(割邊)、點雙和邊雙,分別對應 4 個 Tarjan 算法。
所有算法的時間復雜度均為 O(n + m)。
雙連通分量用到 DFS 樹的性質,所有的邊分別樹邊和返祖邊兩類,大大簡化了代碼。
雙連通分量具有大量的性質,要能熟練掌握。
一些定義:樹枝邊:DFS時經過的邊(由上至下);
返祖邊:與DFS方向相反,從某個節點指向某個祖先的邊;
返祖邊:與DFS方向相反,從某個節點指向某個祖先的邊;
注意:在無向圖中,不能用dfn[fa]更新low[u];所以我們需要標記fa;
但如果有重邊,就可以;所以我們可以記錄它的上一條邊;利用成對儲存的思想記錄上一條邊來判重;
求割點:
割點性質:
(1)根結點如果是割點當且僅當其子節點數大於等於 2;
(2)非根節點 u 如果是割點,當且僅當存在 u 的一個子樹,子樹中沒有連向 u 的祖先的邊(返祖邊)。
(2)非根節點 u 如果是割點,當且僅當存在 u 的一個子樹,子樹中沒有連向 u 的祖先的邊(返祖邊)。
代碼:
void tarjan(int u,int fa) //當fa=0時,說明該節點是根節點; { int num=0; //用來計量子節點數; low[u]=dfn[u]=++cur; for(int i=head[u];i;i=star[i].to){ //鏈式前向星存圖; int v=star[i].to; if(!dfn[v]){ tarjan(v,u); low[u]=min(low[u],low[v]); if(!fa && ++num>1||fa && dfn[u]<=low[v]){ //1.根節點是割點,且子節點數大於等於2; //2.非根節點是割點,且子節點中沒有返祖邊; cutpoint[u]=1; //標記該點為一個割點; } } else if(v!=fa){ low[u]=min(low[u],dfn[v]); } } }
求點雙連通分量:
以下 3 條等價(均可作為點雙連通圖的定義):
(1)該連通圖的任意兩條邊存在一個包含這兩條邊的簡單環;
(2)該連通圖沒有割點;
(3)對於至少3個點的圖,若任意兩點有至少兩條點不重復路徑。
下面兩句話看不看的懂都行:
點雙連通分量構成對所有邊集的一個划分。
兩個點雙連通分量最多只有一個公共點,且必為割點。進一步地,所有點雙與割點可抽象為一棵樹結構。
#include <bits/stdc++.h> using namespace std; struct littlestar{ int to; int nxt; }star[200010]; int head[200010],cnt; void add(int u,int v){ star[++cnt].to=v; star[cnt].nxt=head[u]; head[u]=cnt; } int low[20010],dfn[20010],cur; pair<int,int> st[200010]; int Top,num; vector<int> res[20010]; void tarjan(int u,int fa) { low[u]=dfn[u]=++cur; for(int i=head[u];i;i=star[i].nxt){ //鏈式前向星存圖 int v=star[i].to; int top=Top; if(v!=fa && dfn[u]>dfn[v]){ st[++Top]=make_pair(u,v); //當這條邊並不是通往父親的邊時,並且該點的子 //樹中沒有返祖邊時,將這條邊壓入棧; } if(!dfn[v]){ tarjan(v,u); low[u]=min(low[u],low[v]); if(dfn[u]<=low[v]){ ++num; //num表示第幾個點雙區域(一個圖可能存在多個點雙) for(;Top>top;Top--){ //類似於強連通分量的退棧過程; int x=st[Top].first; int y=st[Top].second; if(res[x].empty() || res[x].back()!=num){ res[x].push_back(num); //由於num遞增,所以res[]遞增,所以res[x]的最后 //如果不是num,就代表之前不會標記過該點; } if(res[y].empty() || res[y].back()!=num){ res[y].push_back(num); //與上面的同理; } } } } else if(v!=fa){ low[u]=min(low[u],dfn[v]); } } }
求橋:
橋的性質: (u; v)邊在dfs 樹中。不妨設u 為v 的父親,v 的子樹沒有向u 或其祖先連的邊。
void tarjan(int u,int fa) { bool flag=0; //用來判斷是否存在重邊 low[u]=dfn[u]=++cur; for(int i=head[u];i;i=star[i].nxt){ int v=star[i].to; if(!dfn[v]){ tarjan(v,u); low[u]=min(low[u],low[v]); if(dfn[v]==low[v]) //它的子節點v的子樹中,沒有像u或其祖先連的邊(返祖邊) { bridge.push_back(i); //橋的一個集合 } } else if(v!=fa || flag){ low[u]=min(low[u],dfn[v]); } else flag=1; } }
求邊雙連通分量
以下3 條等價(均可作為邊雙連通圖的定義):
(1)該連通圖的任意一條邊存在一個包含這條邊的簡單環;
(2)該連通圖沒有橋;
(3)該連通圖任意兩點有至少兩條邊不重復路徑。
下面兩句話看不看的懂都行:
(1)邊雙連通分量構成對所有點集的一個划分。
(2)兩個邊雙連通分量最多只有一條邊,且必為橋。進一步地,所有邊雙與橋可抽象為一棵樹結構。
#include <bits/stdc++.h> using namespace std; struct littlestar{ int to; int nxt; }star[10010]; int head[10010],cnt; void add(int u,int v){ star[++cnt].to=v; star[cnt].nxt=head[u]; head[u]=cnt; } int st[5010],Top,num; int low[5010],dfn[5010],cur; int res[5010]; int kk[150][150]; int anss[5001]; void tarjan(int u,int fa) { bool flag=0; low[u]=dfn[u]=++cur; st[++Top]=u; for(int i=head[u];i;i=star[i].nxt){ int v=star[i].to; if(!dfn[v]){ tarjan(v,u); low[u]=min(low[u],low[v]); } else if(v!=fa || flag){ low[u]=min(low[u],dfn[v]); } else flag=1; } //到此為止與求橋的意義差不多 if(low[u]==dfn[u]){ //u的子樹中,沒有返祖邊 num++; int tmp; do{ tmp=st[Top--]; //退棧,原來棧中的元素構成一個邊雙 res[tmp]=num; }while(tmp!=u); } }