【模板】點雙連通分量、橋連通分量、強連通分量(BCC & SCC) === ## DFS Tree DFS Tree 是將一張圖 DFS 之後構成的一顆樹。 ### 有向圖的 DFS Tree 我們考慮 DFS 一張有向圖,在 DFS 途中,若通過 $u \to v$ 從 $u$ 走到鄰居 $v$,可能有以下情況: 1. Tree Edge:若 $v$ 尚未被造訪過,則會繼續從 $v$ DFS 下去,且 $u \to v$ 為 Tree Edge。而這些邊也就是是在 DFS 過程中實際有走的邊。 當 DFS 結束,Tree Edge 會把所有點連成一顆樹。而剩下的邊便是在 DFS 過程中,某些節點會連向已造訪的節點,所以沒有 DFS 下去的那些邊。分成三種: 1. 若 $v$ 被造訪過,且 $v$ 為 $u$ 在 DFS Tree 上的祖先。此時 $u \to v$ 為 Back Edge。 2. 若 $v$ 被造訪過,且 $v$ 為 $u$ 在 DFS Tree 上的子孫。此時 $u \to v$ 為 Forward Edge。 3. 若 $v$ 被造訪過,但 $v$ 不屬於以上情況,為其他分枝的節點。此時 $u \to v$ 為 Cross Edge。 ### 無向圖的 DFS Tree 而在無向圖中,只存在 Tree Edge 和 Back Edge。 我們考慮 DFS 一張無向圖。如果在從 $u$ 走到 $v$ 時, $v$ 尚未造訪過,那麼會繼續從 $v$ 探訪下去,此時 $u \to v$ 為 Tree Edge。反之若 $v$ 已經造訪過,就不會探訪下去——此時 $u \to v$ 為 Back Edge。 DFS 完成之後,Tree Edge 會形成一張包含所有節點的有根樹。而某些節點,會擁有一條 Back Edge 連回他們在 DFS Tree 上的祖先。 為何無向圖沒有 Forward Edge 或 Cross Edge?在無向圖中 DFS 時,若碰到一鄰居已經參訪過了,那麼他必定還沒有「完成」DFS,也就是該鄰居的 DFS 一定還在 in-stack 的狀態。因此他必定是 DFS Tree 上的祖先。 ## 割點與橋 AP & Bridge 假設有一無向圖 $G$,若將一個點 $u$ 拔除後,會使得連通塊數量變多,那麼該點為「割點(Articulation Point)」。若將一條邊 $(u,v)$ 拔除後,會使得連通塊數量變多,則該邊為「橋邊(Bridge)」。 找 AP 和 Bridge 當然可以暴力拔拔看,但我們有 Tarjan 算法可以在 $O(|V|+|E|)$ 的時間找到所有割點與橋。 ### Low 函數 我們定義 $d(u)$ 為 $u$ 點在 DFS 過程中獲得的「進入」的時間戳。那麼我們知道,在 DFS Tree 當中,$u$ 祖先的時間戳 $d(ancestor)$ 一定比 $d(u)$ 來得小。 我們再定義 $low(u)$ 為 $u$ 不經由通往 parent 的 Tree Edge 時,所能到達的節點中最小的 $d(v)$。不沿著 Tree Edge 往上走,那麼能到達的點就只有:自己、所有子孫、自己以及所有子孫的 Back Edge 連出去的節點。那麼顯然答案只有自己,或是子樹中 Back Edge 的出度。 因此我們可以這麼轉移: $low(u) = min(low(son_u), d(u), d(b_u)))$。其中 $son_u$ 為 $u$ 所有親兒子,$b_u$ 為所有 $u$ 的 Back Edge 連回去的點。 ### Tarjan’s for AP & Bridge Tarjans 算法基於三個定理: 1. 根節點 $r$ 是 AP $\iff$ $deg(r)>1$ 2. $u$ 是非根節點 , $v$ 是 $u$ 的兒子 $u$ 是 AP $\iff$ $\forall v \, low(v) \le d(u)$ 3. $u$ 是非根節點, $v$ 是 $u$ 的兒子 $(u,v)$ 是 Bridge $\iff$ $\forall v \, low(v) < d(u)$ 也能用下面的方法判斷橋,跟上面是等價的: - 若 $u$ 是非根節點,$p$ 是 $u$ 的父親,則: $(u,p)$ 是 Bridge $\iff$ $low(u)=d(u)$ ```cpp void DFS(int v, int fa) //call DFS(v,v) at first { D[v] = L[v] = timestamp++; //timestamp > 0 int childCount = 0; //定理1 for root bool isAP = false; for (int w:adj[v]) { if( w==fa ) continue; if ( !D[w] ) // D[w] = 0 if not visited { DFS(w,v); childCount++; if (D[v]<=L[w]) isAP = true; //定理2 if (D[v]< L[w]) edgeBridge.emplace_back(v, w);//定理3 L[v] = min(L[v], L[w]); } L[v] = min(L[v], D[w]); } if ( v == fa && childCount < 2 ) isAP = false; //定理1, v==fa只是確認root if ( isAP ) nodeAP.push_back(v); return ; } ``` > 練習 **UVa 315,找 AP** > ## 邊雙連通分量 若一個連通分量裡沒有橋,那他就是一個「橋連通分量」。橋連通分量也稱「邊雙連通分量」。在一張連通無向圖中,我們稱 $u,v$ 是邊雙連通的,若且唯若刪除任何一條邊,$u,v$ 皆保持連通。 邊雙連通有傳遞性,因此每個點洽屬於一個邊雙連通分量。每個橋連通分量之間,會以橋作為連接,橋的兩端分別隸屬不同橋連通分量。我們可以在做 Tarjan 時,從下到上的拔除橋連通分量,求出它們。 ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/62e3ee75-c3cc-4ac6-9dd9-09bdec7a116e/Untitled.png) 在 DFS Tree 中,我們一但發現 $u$ 以及其父親 $p$ 會形成橋邊 $(u,p)$,那們 $u$ 便是一個橋的下端點。由於我們子樹中屬於其他橋連通分量的部分已經被拔除,因此 $u$ 僅存的子樹都和 $u$ 屬於同一個橋連通分量。 我們可以通過前序 stack 來維護子樹節點,具體來說,在初造訪 $u$ 時,把 $u$ 放進 stack。在 $u$ 的子孫走訪結束返回到 $u$,並發現 $u$ 為橋的下端點時,將 $u$ 以及還存在 stack 中的子樹都 pop 出來,這些點就隸屬同一個橋連通分量。 ```cpp void DFS(int v, int fa) { //call DFS(v,v) at first D[v] = L[v] = timestamp++; //timestamp > 0 st.emplace(v); for (int w:adj[v]) { if( w==fa ) continue; if ( !D[w] ) { // D[w] = 0 if not visited DFS(w,v); L[v] = min(L[v], L[w]); } L[v] = min(L[v], D[w]); } if (L[v]==D[v]) { bccid++; int x; do { x = st.top(); st.pop(); bcc[x] = bccid; } while (x!=v); } return ; } ``` ## 點雙連通分量 若在一連通無向圖中,我們稱 $u,v$ 是點雙連通的,若且唯若任何一點拔除(不包含 $u,v$),皆不會使得 $u,v$ 不連通。點雙連通不具有傳遞性,因此兩個點雙連通分量可能會共用一個點。 相鄰的點雙連通分量之間以割點做分隔,但是共用割點。因此求點雙連通分量的方法和求邊雙連通類似,在發現割點時將 stack 中剩餘的點 pop 出來,這些點便和該割點同一個點雙連通分量。接著還要再該割點 push 回 stack,因為該割點還會屬於另一個點雙連通分量。 ```cpp stack<tuple<int,int>> st; void DFS(int v, int fa) { //call DFS(v,v) at first D[v] = L[v] = timestamp++; //timestamp > 0 for (int w:adj[v]) { if( w==fa ) continue; if ( !D[w] ) { // D[w] = 0 if not visited st.emplace(v, w); DFS(w,v); L[v] = min(L[v], L[w]); if (L[w] >= D[v]) { // 找到割點! int x, y; bcc.push_back({}); do { tie(x, y) = st.top(); st.pop(); bcc.back().emplace_back(y); } while (tie(x, y) != tie(v, w)); bcc.back().emplace_back(v); } } L[v] = min(L[v], D[w]); } return ; } ``` ## 強連通分量 有一有向圖 $G$,我們稱 $u,v$ 是強連通的(Strong Connected),若且唯若存在 $u$ 到 $v$ 的路徑,也存在 $v$ 到 $u$ 的路徑。也就是,在一個有向環裡的節點,都互相是強連通的。強連通具有傳遞性,因此每個節點恰屬於一個強連通分量(Strong Connected Component) 我們可以將一個 SCC 分量縮成一點,那麼新圖一定會是一張 DAG。若新圖存在環,那麼環上的所有節點應該要屬於同一個 SCC 才對。 ### Kosaraju’s algo 顯然,一個強連通分量的反圖也是一個強連通分量。有一圖 $G$,假設從起點 $r$ DFS 可以走到所有點,那麼使用後序將節點放進序列:若序列裝 $u$ 在 $v$ 前面,代表 $v$ 在 $G$ 中可以通往 $u$。 使 $rG$ 為 $G$ 的反圖。接著從序列的後往前遍歷(以 $u$ 表示),那麼在 $rG$ 中從 $u$ 做起點 DFS,可到達的點在就是在反圖中可以到達(以 $v$ 表示)。若 $v$ 在序列中,排序比 $u$ 前面,則代表 $u, v$ 可以互相抵達,也就是屬於同一個 SCC。 ```cpp void DFS(vector<int> *dG, int v, int k=-1){ visited[v] = true; scc[v] = k; for (int w:dG[v]) if (!visited[w]) DFS(dG,w,k); if(dG==G) st.push(v); } int Kosaraju(int N){ memset(visited,0,sizeof(visited)); for(int i=0; i<N; ++i) if(!visited[i]) DFS(G,i); memset(visited,0,sizeof(visited)); while(!st.empty()){ if (!visited[st.top()]) DFS(GT, st.top(), sccID++); st.pop(); } return sccID; } ``` > 練習題: **Luogu 2863** > ## 圓方樹 ## Reference - [https://slides.com/sylveon/graph-7](https://slides.com/sylveon/graph-7#/3/14/6) - 這裡寫了差分求 AP & Bridge 的方法:https://oi-wiki.org/graph/bcc/ - 程式碼:sylveon