<更新提示>
<第一次更新>
<正文>
無向圖的割點與割邊
定義:給定無相連通圖\(G=(V,E)\)
若對於\(x \in V\),從圖中刪去節點\(x\)以及所有與\(x\)關聯的邊后,\(G\)分裂為兩個或以上不連通的子圖,則稱\(x\)為\(G\)的割點。
若對於\(e \in E\),從圖中刪去邊\(e\)之后,\(G\)分裂為兩個不連通的子圖,則稱\(e\)為\(G\)的割邊。
對於很多圖上問題來說,這兩個概念是很重要的。我們將探究如何求解無向圖的割點與割邊。
預備知識
時間戳
圖在深度優先遍歷的過程中,按照每一個節點第一次被訪問到的順序給\(N\)個節點\(1-N\)的標記,稱為時間戳,記為\(dfn_x\)。
追溯值
設節點\(x\)可以通過搜索樹以外的邊回到祖先,那么它能回到祖先的最小時間戳稱為節點\(x\)的追溯值,記為\(low_x\)。當\(x\)沒有除搜索樹以外的邊時,\(low_x=x\)。
對於追溯值和時間戳,我們可以利用\(dfs\)求解:
- 將\(low\)和\(dfn\)的初始值賦值為當前訪問到的次序
- 訪問當前節點的每一個子節點
- 若當前子節點的\(dfn\)值為\(0\),遞歸子節點,並利用子節點的\(low\)值更新當前節點的\(low\)值
- 若當前子節點的\(dfn\)值非\(0\),則說明這是一條"返祖邊",利用該邊連接的節點的\(dfn\)值更新當前節點的\(low\)值
- 對於能不能通過二元環從子節點直接更新父節點,對於割邊來說 不能,對於割點來說 無所謂。原因我們等一下詳細討論。
Tarjan 算法
著名的\(Tarjan\)算法可以在線性時間內求解無向圖的割點與割邊。
我們來了解一下\(Tarjan\)算法。
割點判定法則
存在\(v\)滿足\(dfn_u\leq low_v(v \in Son(u))\)時,\(u\)為圖的一個割點
特別地,\(u\)為根節點時,需要有兩個子節點\(y_1,y_2\)滿足要求。
證明:
令\(S\)是從根\(r\)到\(u\)的軌上含\(r\)不含\(u\)的一切點組成的集合,\(T\)是以\(v\)為根的子樹上的點集。易知不存在連接\(T\)與\(V-(S∪{u}∪T)\)的邊。若存在連接\(t∈T\)與\(s∈S\)的邊\(ts\),則它是返祖邊,且\(dfn_s<dfn_u\)。這時\(low_v ≤ dfn_s ≤ dfn_u\),與已知\(low_v ≥ dfn_u\)矛盾,故\(ts\)這種邊不存在,故\(u\)是割點,證畢。
我的理解:
對於任意的點\(u\),存在\(v\)滿足\(dfn_u\leq low_v(v \in Son(u))\)時,就說明\(u\)的子節點無論怎樣都無法通過其他邊回到\(u\)的父節點,也就是說,\(u\)以后的部分就相當於"孤立"了,刪去\(u\),圖的聯通分量必然增加。
對於之前能否通過二元環更新的問題,驗證可知,無論能否更新,\(dfn_u\leq low_v\)都可以滿足,原來的割點不會漏判,也不會多判,不受影響。
依此,我們可以用遞歸的形式實現求解\(dfn\)以及\(low\)數組,並利用判定法則找到割點。
\(Code:\)
inline void Tarjan(int x,int root)
{
dfn[x]=low[x]=++cnt;
int flag=0;
for(int i=Last[x];i;i=e[i].next)
{
int y=e[i].ver;
if(!dfn[y])
{
Tarjan(y,root);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x])
{
flag++;
if(x!=root||flag>1)cutvertex[x]=true;
}
}
else low[x]=min(low[x],dfn[y]);
}
}
割邊判定法則
存在\(v\)滿足\(dfn_u < low_v(v \in Son(u))\)時,\((u,v)\)為圖的一條割邊
我們可以用類似於割點判定法則的方法證明割邊判斷法則,理解也基本相同,這里不再贅述。
值得我們注意的是,在求解割點時,由於判定法則是:
存在\(v\)滿足\(dfn_u\leq low_v(v \in Son(u))\)時,\(u\)為圖的一個割點
所以我們不在乎程序實現時\(v\)是否會枚舉到\(u\)的父親節點(不會對答案造成影響),但是,我們在求割邊時,\(v\)枚舉到\(u\)的父親會帶來錯誤(沒有等號,會漏判割邊),所以我們利用異或運算和"成對變換"的技巧避免通過無向邊回到父親節點(也稱位運算卡掉二元環)。
具體地,如\(2\ xor\ 1=3,3\ xor\ 1=2\),我們將兩條有向邊組成的無向邊存在\(2\),\(3\)兩個下標中,得以互相轉換。
\(Code:\)
inline void Tarjan(int x,int inedge)
{
dfn[x]=low[x]=++cnt;
for(int i=Last[x];i;i=e[i].next)
{
int y=e[i].ver;
if(!dfn[y])
{
Tarjan(y,i);
low[x]=min(low[x],low[y]);
if(low[y]>dfn[x])
bridge[i]=bridge[i^1]=true;
}
else if(i!=(inedge^1))
low[x]=min(low[x],dfn[y]);
}
}
交換機
Description
n個城市之間有通訊網絡,每個城市都有通訊交換機,直接或間接與其它城市連接。因電子設備容易損壞,需給通訊點配備備用交換機。
但備用 交換機數量有限,不能全部配備,只能給部分重要城市配置。
於是規定:如果某個城市由於交換機損壞,不僅本城市通訊中斷,還造成其它城市通訊中斷,則配備備 用交換機。
請你根據城市線路情況,計算需配備備用交換機的城市個數,及需配備備用交換機城市的編號。
友情提示:圖論常見的坑點,重邊,自環,還有對本題來說的不連通
Input Format
第一行,一個整數n,表示共有n個城市(2<=n<=20000)
下面有若干行(<=60000):每行2個數a、b,a、b是城市編號,表示a與b之間有直接通訊線路。
Output Format
第一行,1個整數m,表示需m個備用交換機。
下面有m行,每行有一個整數,表示需配備交換機的城市編號。
輸出順序按編號由小到大。如果沒有城市需配備備用交換機則輸出0。
Sample Input
7
1 2
2 3
2 4
3 4
4 5
4 6
4 7
5 6
6 7
Sample Output
2
2
4
解析
這是一道\(Tarjan\)求割點模板題,借此給出\(Tarjan\)求割點的完整代碼。
\(Code:\)
#include<bits/stdc++.h>
using namespace std;
const int N=90000,M=150000;
int n,Last[M],t,cutvertex[N],dfn[N],low[N],cnt,m,ans[N];
struct edge{int ver,next;}e[M];
inline void insert(int x,int y)
{
e[++t].ver=y;e[t].next=Last[x];Last[x]=t;
}
inline void input(void)
{
scanf("%d",&n);
int x,y;
while(~scanf("%d%d",&x,&y))
if(x^y)insert(x,y),insert(y,x);
}
inline void Tarjan(int x,int root)
{
dfn[x]=low[x]=++cnt;
int flag=0;
for(int i=Last[x];i;i=e[i].next)
{
int y=e[i].ver;
if(!dfn[y])
{
Tarjan(y,root);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x])
{
flag++;
if(x!=root||flag>1)cutvertex[x]=true;
}
}
else low[x]=min(low[x],dfn[y]);
}
}
int main(void)
{
input();
for(int i=1;i<=n;i++)
if(!dfn[i])Tarjan(i,i);
for(int i=1;i<=n;i++)
{
if(cutvertex[i])
ans[++m]=i;
}
if(m)printf("%d\n",m);
else printf("0\n");
for(int i=1;i<=m;i++)
printf("%d\n",ans[i]);
}
危險道路
Description
天凱是蘇聯的總書記。蘇聯有n個城市,某些城市之間修築了公路。任意兩個城市都可以通過公路直接或者間接到達。 天凱發現有些公路被毀壞之后會造成某兩個城市之間無法互相通過公路到達。這樣的公路就被稱為dangerous pavement。 為了防止美帝國對dangerous pavement進行轟炸,造成某些城市的地面運輸中斷,天凱決定在所有的dangerous pavement駐扎重兵。可是到底哪些是dangerous pavement呢?你的任務就是找出所有這樣的公路。
Input Format
第一行n,m(1<=n<=100000, 1<=m<=300000),分別表示有n個城市,總共m條公路。
以下m行每行兩個整數a, b,表示城市a和城市b之間修築了直接的公路。
Output Format
輸出有若干行。每行包含兩個數字a,b(a < b),表示 < a,b >是dangerous pavement。請注意:輸出時,所有的數對< a,b>必須按照a從小到大排序輸出;如果a相同,則根據b從小到大排序。
Sample Input
6 6
1 2
2 3
2 4
3 5
4 5
5 6
Sample Output
1 2
5 6
解析
這是一道\(Tarjan\)求割邊模板題,借此給出\(Tarjan\)求割邊的完整代碼。
\(Code:\)
#include<bits/stdc++.h>
using namespace std;
const int N=100000+200,M=300000+200;
int n,m,t=1,Last[M*2],dfn[N],low[N],bridge[N],cnt,tot;
struct edge{int ver,next;}e[M*2];
pair < int,int > ans[N];
inline void insert(int x,int y)
{
e[++t].ver=y;e[t].next=Last[x];Last[x]=t;
}
inline void input(void)
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
insert(x,y);
insert(y,x);
}
}
inline void Tarjan(int x,int inedge)
{
dfn[x]=low[x]=++cnt;
for(int i=Last[x];i;i=e[i].next)
{
int y=e[i].ver;
if(!dfn[y])
{
Tarjan(y,i);
low[x]=min(low[x],low[y]);
if(low[y]>dfn[x])
bridge[i]=bridge[i^1]=true;
}
else if(i!=(inedge^1))
low[x]=min(low[x],dfn[y]);
}
}
int main(void)
{
input();
for(int i=1;i<=n;i++)
if(!dfn[i])Tarjan(i,0);
for(int i=2;i<t;i+=2)
if(bridge[i])
{
if(e[i].ver>e[i^1].ver)swap(e[i].ver,e[i^1].ver);
ans[++tot]=make_pair(e[i].ver,e[i^1].ver);
}
sort(ans+1,ans+tot+1);
for(int i=1;i<=tot;i++)
printf("%d %d\n",ans[i].first,ans[i].second);
return 0;
}
<后記>