更好的閱讀體驗&驚喜&原文鏈接
感謝@yxc的腿部掛件 大佬,指出本文不夠嚴謹的地方,萬分感謝!
Tarjan無向圖的割點和橋(割邊)
導言
在掌握這個算法前,咱們有幾個先決條件.
- [x] DFS搜索
- [x] DFS序
- [x] 一張紙
- [x] 一支筆
- [x] 認真的大腦
(滑稽)
如果您都具備了,那么您就是巨佬了,您就可以輕松解決Tarjan算法了.
初學算法
概念掌握
割點
概念定義什么的,看上去好煩好煩好煩的,怎么辦呢?
Acwing小劇場開播了,門票一枚AC幣.
現在Acwing推出了一款戰略游戲,名為Tarjan無向圖的割點和橋.
貪玩Tarjan,介個是泥從未丸過的船新版本.
整個游戲,由\(N\)個島嶼構成,而且有\(M\)條航線聯絡着這些島嶼.
我們發現熊熊助教
和y總
這兩個點非常重要,是整張地圖的核心戰略要塞.
假如說缺少了任意一個點,我們發現總會有兩個島嶼,不能聯系了.
因此我們得出了,核心戰略要塞,就是交通聯絡點.
所以這些點要重要中葯保護,避免被破壞,
因此我們把這些要重要保護的點,也就是交通聯絡點,稱之為割點.
概念 | 定義 |
---|---|
割點 | 刪掉這個點和這個點有關的邊,圖就不是連通圖,分裂成為了多個不相連的子圖 |
割邊
同樣的有核心戰略要塞,也就會有黃金水道.
什么是黃金水道呢?
難道是航運量大的航道?
不不不不,這個概念不一樣.
如果說黃金水道被破壞了,那么將會有兩個和兩個以上的島嶼,不能夠通航連接了.
比如說,熊熊助教
和y總
連接的一條航道.
還有熊熊助教
和song666
連接的航道.
這就是我們的黃金水道,也就是戰略航道.
因此我們給這些戰略航道定義為橋(割邊).
概念 | 定義 |
---|---|
橋 | 刪除這條邊后,圖就不是連通圖,分裂成為多個不相連的子圖. |
時間戳
其實啊,我們完全可以給這些島嶼們編號.這樣便於管理,有利於愉悅身心健康.
因此我們得出了下面這張圖片.
我們發現刪除了一些多余的邊,然后每個點多了一個學號.
其實我們的學號,就是每一個節點的時間戳.
什么是時間戳,就是每一個點的學號.
但是這個學號是怎么來的呢?總不能是一頓瞎排的吧.
其實這個學號,就是我們的DFS序,剛開始我們任意選擇一個點開始,然后一次深度優先搜索,每新搜索到一個點后,就給這個點標記一個學號.
然后來一個gif動畫看一看.
因此時間戳的定義我們就知道了.
概念 | 定義 |
---|---|
時間戳 | \(dfn[x]\)表示節點x第一次被訪問的編號. |
這就是時間戳的概念,其實就是學號編輯的過程.
追溯值
追溯,追溯,就是尋找一個節點,以他為根,可以抵達的最小學號.
我們設置一個小概念
比如說我們舉一個例子.
這些紅色標記節點,其實也就是熊熊助教的搜索樹.
因此我們得出.
那么我們設置一下追溯值數組.
- \(subtree(x)\)中的節點
- 通過\(1\)條不在搜索樹上的邊,能夠抵達\(subtree(x)\)中的節點.
這個第一條我們上面解釋過了,那么第二條怎么解釋呢,還是一樣,我們再來一個解釋gif.
判定法則
割邊判斷法則
無向邊\((x,y)\)是橋,當且僅當搜索樹上存在\(x\)的一個子節點\(y\),滿足
首先一個公式,很難讓人理解,我們得有一點人性化理解.
橋是干什么的?
它是用來連接兩個個連通塊的,沒有了橋,就沒有連通性,就會出現世外桃源.
什么是世外桃源,就是自成一派,與外人無往來.
我們需要知道追溯值,是什么.
就是一個節點可以在自己子樹和子樹可以拓展的節點中找到一個最小的編號.
刪掉了橋,那么在世外桃源,請問對於任何一個節點而言,他們存在,一個可以往外面拓展的節點嗎?
沒有,因為他們是世外桃源,不與外人有任何連接.
於是世外桃源內所有的節點,他們的最小追溯值一定不會小於宗主的編號.
咱們要知道,自給自足是很難成功的,總得有一個人出去買加碘海鹽,那么這個人就是吃貨宗的宗主
我們認為宗主就是所有節點中編號最小的節點,也就是有可能與外人有所連接的節點.
換句話說,也就是\((x,y)\)這個橋兩端中,在世外桃源內的節點就是宗主\(y\).
正經語言說一說就是.
因此當\(dfn[x]<low[y]\)的時候
- 我們發現從\(y\)節點出發,在不經過\((x,y)\)的前提下,不管走哪一條邊,我們都無法抵達\(x\)節點,或者比\(x\)節點更早出現的節點
- 此時我們發現\(y\)所在的子樹似乎形成了一個封閉圈,那么\((x,y)\)自然也就是橋了.
割點判斷法則
其實和上面的判斷,只有一點修改.
若\(x\)不是搜索樹的根節點,若\(x\)節點是割點,那么當且僅當搜索樹上存在\(x\)的一個兒子節點\(y\),滿足
宗主節點,是所有人中實力最強大的,所以肯定是最先學習的.
既然如此,那么顯然他的\(dfn\),就是代表學習的開始時間,必然就是最小的.
而且割點是一個世外桃源和外界的唯一通道,所有的兒子節點的\(dfn\)都必須大於等於它,不可以小於它,因此證畢.
其實證明和上面的正經證明是一模一樣的,只不過多了一個等於號罷了.
特殊定義:請記住根是不是割點,必須保證有至少兩個兒子節點,否則不叫作割點.
代碼解析
割邊模板
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+20;
int head[N],edge[N<<1],Next[N<<1],tot;
int dfn[N],low[N],n,m,num;
bool bridge[N<<1];
void add_edge(int a,int b)
{
edge[++tot]=b;
Next[tot]=head[a];
head[a]=tot;
}
void Tarjan(int x,int Edge)
{
dfn[x]=low[x]=++num;//DFS序標記
for(int i=head[x]; i; i=Next[i])//訪問所有出邊
{
int y=edge[i];//出邊
if (!dfn[y])//不曾訪問過,也就是沒有標記,可以認為是兒子節點了
{
Tarjan(y,i);//訪問兒子節點y,並且設置邊為當前邊
low[x]=min(low[x],low[y]);//看看能不能更新,也就是定義中的,subtree(x)中的節點最小值為low[x]
if (low[y]>dfn[x]) //這就是橋的判定
bridge[i]=bridge[i^1]=true;//重邊也是橋
} else if (i!=(Edge^1))
low[x]=min(low[x],dfn[y]);//第二類定義,也就是通過1條不在搜索樹上的邊,能夠抵達subtree(x)的節點
}
}
int main()
{
scanf("%d%d",&n,&m);
tot=1;//邊集從編號1開始
for(int i=1; i<=m; i++)
{
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
for(int i=1;i<=n;i++)
if (!dfn[i])//一個無向圖,可能由多個搜索樹構成
Tarjan(i,0);
for(int i=2;i<=tot;i+=2)//無向邊不要管,直接跳2格
if (bridge[i])
printf("%d %d\n",edge[i^1],edge[i]);//橋的左右兩端
return 0;
}
割點模板
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+20;
int head[N],edge[N<<1],Next[N<<1],tot;
int dfn[N],low[N],n,m,num,root,ans;
bool cut[N];
void add_edge(int a,int b)
{
edge[++tot]=b;
Next[tot]=head[a];
head[a]=tot;
}
void Tarjan(int x)
{
dfn[x]=low[x]=++num;//DFS序標記
int flag=0;
for(int i=head[x]; i; i=Next[i])//訪問所有出邊
{
int y=edge[i];//出邊
if (!dfn[y])//不曾訪問過,也就是沒有標記,可以認為是兒子節點了
{
Tarjan(y);//訪問兒子節點y,並且設置邊為當前邊
low[x]=min(low[x],low[y]);//看看能不能更新,也就是定義中的,subtree(x)中的節點最小值為low[x]
if (low[y]>=dfn[x]) //這就是割點的判定
{
flag++;//割點數量++
if (x!=root || flag>1)//不能是根節點,或者說是根節點,但是有至少兩個子樹節點是割點
cut[x]=true;
}
}
else low[x]=min(low[x],dfn[y]);//第二類定義,也就是通過1跳不在搜索樹上的邊,能夠抵達subtree(x)的節點
}
}
int main()
{
scanf("%d%d",&n,&m);
memset(cut,false,sizeof(cut));
for(int i=1; i<=m; i++)
{
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
for(int i=1; i<=n; i++)
if (!dfn[i])//一個無向圖,可能由多個搜索樹構成
root=i,Tarjan(i);
for(int i=1; i<=n; i++) //統計割點個數
if (cut[i])
ans++;
printf("%d\n",ans);
for(int i=1; i<=n; i++) //順序遍歷,康康哪些點是割點
if (cut[i])
printf("%d ",i);
return 0;
}
經典題目
第一題 B城
題目描述
B城有 \(n\) 個城鎮,\(m\) 條雙向道路。
每條道路連結兩個不同的城鎮,沒有重復的道路,所有城鎮連通。
把城鎮看作節點,把道路看作邊,容易發現,整個城市構成了一個無向圖。
輸入格式
第一行包含兩個整數 \(n\) 和 \(m\)。
接下來\(m\)行,每行包含兩個整數 \(a\) 和 \(b\),表示城鎮 \(a\) 和 \(b\) 之間存在一條道路。
輸出格式
輸出共\(n\)行,每行輸出一個整數。
第 \(i\) 行輸出的整數表示把與節點 \(i\) 關聯的所有邊去掉以后(不去掉節點 \(i\) 本身),無向圖有多少個有序點\((x,y)\),滿足$ x$ 和$ y$ 不連通。
數據范圍
輸入樣例:
5 5
1 2
2 3
1 3
3 4
4 5
輸出樣例:
8
8
16
14
8
解題報告
題意理解
一張圖,每次刪除一個節點,包括它和其他節點的邊,問刪除這個節點過后,會有多少個有序節點\((x,y)\)之間無法連通.
Hint:有序節點\((x,y)\)和有序節點\((y,x)\)不是同樣的節點對.我們要計算兩次.
算法解析
刪除一個節點,使得圖不連通,這不就是割點的定義嗎?
- 假如刪掉的節點不是割點.
此時我們發現,除了這個節點與其他節點都不連通,其他節點都是連通的.
因此答案為.
也就是除了當前節點,其余的\((n-1)\)個節點都與當前節點不連通.(自己和自己當然是連通的)
然后答案要計算兩遍,因此\((n-1) \times 2\)
- 假如說刪除的節點是割點
刪除割點,會使得圖變得四分五裂,成為了若干個連通塊.
那么連通塊本身內部的節點,當然還是互相連通的.
但是兩個不同的連通塊的節點,顯然就不連通了.
比如說\(a\)節點屬於\(1\)號連通塊.
然后\(b\)節點屬於\(2\)號連通塊.
請問他們連通嗎?
不連通!
所以答案+1.
那么我們從特解到通解.
假如說此時有\(1,2,3,4,5\)這五個連通塊.
我們提出一個概念.
而且\(s\)節點屬於\(1\)號連通塊.
那么除了自己所在\(1\)號連通塊內部節點與自己連通,其他連通塊節點和自己都沒有任何關系.
因此我們得出如下公式.
顯然與\(s\)節點相連的節點個數有
那么與\(s\)節點不連通的節點個數有
那么\(s\)節點的貢獻是什么呢?
因此我們推出一個連通塊貢獻的價值.
但是一個割點顯然不會只有1個連通塊,我們假設它有
因此一個割點的子連通塊貢獻了.
所有兒子連通塊+自身,一共有多少個節點呢,我們算一下.
但是根據搜索樹定義,割點有自己的兒子連通塊,當然也就有不是兒子連通塊.
因此我們得出不是兒子連通塊的個數為.
其實也就是.
那么這些不是兒子連通塊,顯然與是兒子聯通塊是不連通了,因為刪除這個割點,所以貢獻代價為
此時我們還要知道,節點自身也是有貢獻的,畢竟和其他節點都不連通.
那么總和上面貢獻,就是最終答案了.我就不打了,上面很清晰了,下面也有代碼解釋.
代碼解析
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=100000*2+200;
long long head[N],edge[N],Next[N],tot,num,ans[N],root;
long long n,m,size[N],dfn[N],low[N];
bool cut[N];
void add_edge(int a,int b)//加邊
{
edge[++tot]=b;
Next[tot]=head[a];
head[a]=tot;
}
void Tarjan(int x)
{
dfn[x]=low[x]=++num;//編號
size[x]=1;//初始時候就自己這一個孤寡老人
int flag=0,sum=0;
for (int i=head[x]; i; i=Next[i])
{
int y=edge[i];//兒子節點
if (!dfn[y])//不曾訪問
{
Tarjan(y);//訪問
size[x]+=size[y];//兒子節點貢獻的子樹大小
low[x]=min(low[x],low[y]);//更新一下
if (low[y]>=dfn[x])//發現了割點
{
flag++;
ans[x]+=size[y]*(n-size[y]);//加上y兒子連通塊帶來的貢獻
sum+=size[y];//這里是統計兒子連通塊總節點數
if (x!=root || flag>1)//是割點
cut[x]=true;
}
}
else
low[x]=min(low[x],dfn[y]);//更新
}
if (cut[x])//是割點
ans[x]+=(n-sum-1)*(sum+1)+(n-1);//非兒子連通塊的貢獻+自身節點貢獻
else
ans[x]=2*(n-1);//不是割點
}
signed main()
{
scanf("%lld%lld",&n,&m);
memset(cut,false,sizeof(cut));//剛開始都不是割點
for(int i=1; i<=m; i++)
{
int a,b;
scanf("%lld%lld",&a,&b);
if (a==b)//無用邊
continue;
add_edge(a,b);
add_edge(b,a);
}
root=1;//根節點為1
Tarjan(1);
for(int i=1; i<=n; i++)
printf("%lld\n",ans[i]);
return 0;
}