【筆記/模板】割點和橋

ThySecret發表於2024-11-04

割點

對於一張無向圖 \(G=(V,E)\),使得 H 是 G 的連通子圖,且不存在 \(F\) 滿足 \(H \subsetneq F \in G\)\(F\) 為連通圖,則稱 \(H\)\(G\) 的一個連通塊/連通分量connected component),又叫極大連通子圖

由此,我們可以對割點做出如下定義:

對於一個無向圖,如果把一個點刪除後這個圖的極大連通分量數增加了,那麼這個點就是這個圖的割點(又稱割頂)。

Tarjan

處理過程

與有向圖的強連通分量相似,我們定義如下兩個重要陣列:

  • dfn[]:表示深度優先遍歷圖時節點 \(u\) 被遍歷到的次序,即時間戳。
  • low[]:節點 \(u\) 在不透過父親走過的路的情況下可以到達節點的最小時間戳。

根據割點的定義可知,如果一個節點 \(ver\) 是割點,那麼必然有一個處於其 DFS 序子樹內的節點 \(to\) 使得:

\[low_{to} \ge dfn_{ver} \]

這個意思指的是無論如何走,\(to\) 都無法到達 \(ver\) 以上的節點,不能回到祖先,那麼 \(ver\) 即為割點。

然而當 \(ver\) 作為搜尋的起始點時,由於 \(ver\) 以上沒有父親節點,自然無法判斷其是否是割點,這種方法並不適用,對於這種情況,我們可以直接特判:記錄從 \(ver\) 節點開始可以到達的子節點數量,如果大於 \(1\),則 \(ver\) 肯定是割點;而如果只有一個兒子的話,刪掉 \(ver\) 不會發生任何影響。

考慮如何在搜尋的過程不斷更新 low[] 陣列:

  1. 當遍歷到當前點 \(ver\) 時,令 \(low_{ver} \gets dfn_{ver}\)

  2. 遍歷子節點 \(to\) 時,如果 \(to\) 已經被更新過(\(dfn_{to}\) 有值),那麼邊 \(ver \to to\) 為非樹邊,要麼為前向邊(我們不用處理),要麼為返祖邊(令 \(low_{ver} \gets dfn_{to}\)​)。

  3. 否則,由於 \(to\) 沒有被遍歷過,在 \(ver\) 的搜尋子樹當中,我們繼續搜尋 to 的子樹,並將傳遞的值返回,令 \(low_{ver} \gets \min(low_{ver}, low_{to})\)

虛擬碼如下(摘自 OI Wiki):

\[\begin{array}{ll} 1 & \textbf{if } v \text{ is a son of } u \\ 2 & \qquad \text{low}_u = \min(\text{low}_u, \text{low}_v) \\ 3 & \textbf{else} \\ 4 & \qquad \text{low}_u = \min(\text{low}_u, \text{dfn}_v) \\ \end{array} \]

注意

由於 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\)

\[low_{to} \gt dfn_{ver} \Leftto egde \in \text{Bridge} \]

需要注意的是,在標記一條邊時,同時要標記它的反邊。

程式碼

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)

相關文章