割點
對於一張無向圖 \(G=(V,E)\),使得 H 是 G 的連通子圖,且不存在 \(F\) 滿足 \(H \subsetneq F \in G\) 且 \(F\) 為連通圖,則稱 \(H\) 是 \(G\) 的一個連通塊/連通分量(connected component),又叫極大連通子圖。
由此,我們可以對割點做出如下定義:
對於一個無向圖,如果把一個點刪除後這個圖的極大連通分量數增加了,那麼這個點就是這個圖的割點(又稱割頂)。
Tarjan
處理過程
與有向圖的強連通分量相似,我們定義如下兩個重要陣列:
dfn[]
:表示深度優先遍歷圖時節點 \(u\) 被遍歷到的次序,即時間戳。low[]
:節點 \(u\) 在不透過父親走過的路的情況下可以到達節點的最小時間戳。
根據割點的定義可知,如果一個節點 \(ver\) 是割點,那麼必然有一個處於其 DFS 序子樹內的節點 \(to\) 使得:
這個意思指的是無論如何走,\(to\) 都無法到達 \(ver\) 以上的節點,不能回到祖先,那麼 \(ver\) 即為割點。
然而當 \(ver\) 作為搜尋的起始點時,由於 \(ver\) 以上沒有父親節點,自然無法判斷其是否是割點,這種方法並不適用,對於這種情況,我們可以直接特判:記錄從 \(ver\) 節點開始可以到達的子節點數量,如果大於 \(1\),則 \(ver\) 肯定是割點;而如果只有一個兒子的話,刪掉 \(ver\) 不會發生任何影響。
考慮如何在搜尋的過程不斷更新 low[]
陣列:
-
當遍歷到當前點 \(ver\) 時,令 \(low_{ver} \gets dfn_{ver}\)。
-
遍歷子節點 \(to\) 時,如果 \(to\) 已經被更新過(\(dfn_{to}\) 有值),那麼邊 \(ver \to to\) 為非樹邊,要麼為前向邊(我們不用處理),要麼為返祖邊(令 \(low_{ver} \gets dfn_{to}\))。
-
否則,由於 \(to\) 沒有被遍歷過,在 \(ver\) 的搜尋子樹當中,我們繼續搜尋 to 的子樹,並將傳遞的值返回,令 \(low_{ver} \gets \min(low_{ver}, low_{to})\)。
虛擬碼如下(摘自 OI Wiki):
注意
由於 low[]
陣列在 DFS 序子樹中具有傳遞性,直接在子樹中傳遞 low[ver] = min(low[ver], low[to])
是沒有任何問題的。
而如果邊 \(ver \to to\) 是一條返祖邊時,採取 low[ver] = min(low[ver], low[to])
的更新方式會使得之後的點可以回溯到 \(ver\) 的其他點更新時重複上跳,違反了 low[]
陣列的初始定義,導致出錯。況且 low[]
陣列的核心用處是判斷 \(low_{to} \ge dfn_{ver}\) ,並非求最值,它僅需要找到一個比 \(ver\) 更小的點即可,因此 low[ver] = min(low[ver], dfn[to])
的正確性成立。
詳見:關於有向圖 Tarjan 演算法模板的一些疑問
程式碼
P3388 【模板】割點(割頂)
給出一個 \(n\) 個點,\(m\) 條邊的無向圖,求圖的割點。
void tarjan(int ver, int pre)
{
dfn[ver] = low[ver] = ++ timestamp; // 初始化時間戳
int child = 0; // 記錄該節點為根有幾個兒子
for (int i = h[ver]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
child ++;
tarjan(j, ver);
low[ver] = min(low[ver], low[j]);
if (ver != pre && low[j] >= dfn[ver] && !flag[ver])
res ++, flag[ver] = true; // 找到一個割點
}
else if (j != pre)
low[ver] = min(low[ver], dfn[j]);
}
if (ver == pre && child >= 2 && !flag[ver])
res ++, flag[ver] = true; // 特判根節點
}
割邊(橋)
與割點的定義類似,如下:
對於一個無向圖,如果刪掉一條邊後圖中的連通分量數增加了,則稱這條邊為橋或者割邊。
Tarjan
處理過程
與 Tarjan 演算法求解割點的方法類似,我們同樣定義兩個陣列 dfn[]
和 low[]
處理,對於一條割邊,以它連線的子樹必然無法透過別的邊到達它上面的點,即對於一條在 DFS 樹上的 \(ver \to to\) 邊 \(edge\):
需要注意的是,在標記一條邊時,同時要標記它的反邊。
程式碼
void tarjan(int ver, int from)
{
dfn[ver] = low[ver] = ++ timestamp;
for (int i = h[ver]; ~i; i = ne[i])
{
int to = e[i];
if (to == (from ^ 1)) continue;
if (!dfn[to])
{
tarjan(to, i);
low[ver] = min(low[ver], low[to]);
if (low[to] > dfn[ver])
bridge[i] = bridge[i ^ 1] = true, ++ ans;
}
else low[ver] = min(low[ver], dfn[to]);
}
}
Reference
割點和橋 - OI Wiki (oi-wiki.org)
P3388 【模板】割點(割頂) - 洛谷 | 電腦科學教育新生態 (luogu.com.cn)
圖論基礎 - qAlex_Weiq - 部落格園 (cnblogs.com)