割頂(橋)學習筆記
最近在學割頂,於是決定寫個筆記。
關於割頂和橋
關於求法
先看定義:割頂(割點)指刪去這個節點後整個圖的連通分量數會增加的節點。
比如這個圖:
割頂就是節點 \(3,5,6\)。
於此對應的就是橋了,橋指刪去這條邊以後整個圖的連通分量數會增加的邊。
在剛才那張圖中,橋就是節點 \(2,6\)、\(5,6\)、\(3,4\) 之間的邊。
接著就是我們該怎麼求割頂了。
先想最樸素的演算法,直接暴力列舉每個點,然後遍歷整個圖,時間複雜度 \(\mathcal{O}(n(n+m))\)
顯然,我們的效率太低了,那我們能不能考慮一種遍歷整個圖就求出來的演算法呢?
這就是 tarjan 演算法了。
首先,我們先來看搜尋樹的概念。
當我們在進行dfs遍歷整個圖時,會產生一棵樹,也就是我們所說的搜尋樹了,比如剛才的圖對應的搜尋樹就是:
我們看到,對於這棵樹來說,多出了一條邊,即 \(1,5\) 間的邊,這條邊我們就稱為反向邊,其餘的邊,我們就稱為樹邊。
在這裡節點 \(3,5\) 是割頂,我們很容易發現如果一個節點,那麼這個節點一定存在一個子節點及其後代都不存在連向這個節點的祖先節點的反向邊。對於根節點,只要他的兒子數大於等於 \(2\) 就能說明是割頂了。
拿 \(3\) 舉個例子,\(3\) 的孩子 \(4\) 沒有連向其祖先節點的反向邊,所以 \(3\) 是割頂。
接著我們再來看一個概念:時間戳。
我們在訪問到每一個節點時時間都不同。於是每個節點的時間戳也都不一樣,比如對於上面那棵搜尋樹來說,節點 \(1\)
反向邊有個性質,我們在這裡反向邊連的兩個節點一定滿足 \(dfn[u] > dfn[v]\)。
結合前面說的,我們在這裡還需要一個輔助陣列 \(low[u]\) 表示節點 \(u\) 及其所有後代能連的最遠的祖先節點(即節點 \(u\) 及其子孫的反向邊所能連到的最小的 \(dfn[v]\) 值)。
如果這個還不能理解的話,可以舉個例子,比如剛才那個搜尋樹,其中 \(low[3]=low[5]=1\)
於是我們的大體演算法就想出來了,只需要遍歷完整個圖就行了,對於每個 \(dfn[u]\le low[v]\) 且不是根節點的節點都是割頂,根節點的兒子數大於 \(1\) 的也是割頂,時間複雜度 \(\mathcal{O}(n+m)\)。
Code:
void tarjan(int u,int pa){
pre[u]=low[u]=++num;
int cnt=0;
for(int i=0;i<G[u].size();++i){
int v=G[u][i];
if(!pre[v]){
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=pre[u]){
++cnt;
if(cnt>1||u!=root)
cut[u]=1;
}
}
else if(v!=pa)low[u]=min(pre[v],low[u]);
}
}
關於應用
一道很模板的題。
斷掉這個節點後一定會有的是 \(2\times(n-1)\),如果這個節點不是割點,那肯定就這麼多了,如果這個點是割點,則不只這個數了。
看這個例子,假如我們刪去了節點 \(3\) 則對於上面那個子圖來說形成了 \(2\times(9-2)\) 種情況,對左邊那個子圖來說則形成了 \(3\times(9-3)\) 種情況,對右邊的子圖來說則也形成了 \(3\times(9-3)\) 種情況。
於是發現刪去這個節點後的方案數就是
\[\sum_{v\in son(u)}siz[u]\times(n-siz[u]) \]在這裡 \(siz[u]\) 即節點 \(u\) 在搜尋樹的子樹大小,除此以外,還要記得帶上父親節點的。
於是只要在 tarjan 的同時維護一下 \(siz[u]\) 就行了。
關於DCC
DCC即雙連通分量,有點和邊兩種,即 v-DCC 和 e-DCC 兩種。
其實很簡單,雙連通分量就是刪去一個東西依然連通,比如這個:
\(1,2,3\) 就是一個 v-DCC,而 \(4,5,6\) 也是 v-DCC,因為無論刪去哪個節點這個連通分量都依然連通。
\(7,8,9\) 是一個 e-DCC,因為無論刪去這個連通分量裡的哪條邊這個連通分量依然連通。
求法
對於 e-DCC,我們只要刪去所有的橋就行了。
對於 v-DCC 要麻煩一些,因為有些點可能屬於很多個不同的 v-DCC。
比如節點 \(3\),既屬於 \(1,2,3\) 的 v-DCC,也屬於 \(3,4\) 和 \(3,7\) 兩個 v-DCC。
我們來看上面那個圖的搜尋樹。
根據定義,我們可以看出,\(1,2,3\),\(4,5,6\),\(7,8,9\) 都是 v-DCC。除此以外,還有 \(3,4\),\(3,7\),發現都與割頂有關係,並且一定與滿足 \(dfn[u]\le low[v]\) 割頂的兒子有關。
於是我們只需要一個棧,在訪問到每個節點時我們把這個節點進棧,在遇到滿足 \(dfn[u]\le low[v]\) 的節點時,我們要進行出棧,一直出到節點 \(u\) 的前一個,最後還要把 \(u\) 放到這個 v-DCC 裡。
void tarjan(int u,int pa){
int child=0;
low[u]=dfn[u]=++num;
stack[++top]=u;
for(int i=0;i<G[u].size();++i){
int v=G[u][i];
if(!dfn[v]){
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(dfn[u]<=low[v]){
++cnt;
dcc[cnt].clear();
++child;
if(child>1||u!=1)cut[u]=1;
int z;
do{
z=stack[top--];
dcc[cnt].push_back(z);
}while(z!=v);
dcc[cnt].push_back(u);
}
}
else if(v!=pa)low[u]=min(low[u],dfn[v]);
}
}
關於應用
先求出所有的 v-DCC,一定不能在割頂上修出口,因為如果塌在割點上就白修了,接著容易發現如果一個 v-DCC 裡存在兩個以上的割頂則不需要修出口,方案數也好求,乘法原理做一下就好了。