Tarjan Algorithm
List
Knowledge
基本知識
Tarjan 主要是用來求有向圖的強連通分量(縮點)和無向圖的橋和割頂。首先都是要求出 DFN 和 LOW 值:DFN 是指一個點被搜索的次序編號,LOW 是指一個點的子樹中最小編號。
基本概念
- 割點:若刪掉某點後,原連通圖分裂爲多個子圖,則稱該點爲割點。
- 割點集合:在一個無向連通圖中,如果有一個頂點集合,刪除這個頂點集合,以及這個集合中所有頂點相關聯的邊以後,原圖變成多個連通塊,就稱這個點集爲割點集合。
- 點連通度:最小割點集合中的頂點數。
- 割邊(橋):刪掉它之後,圖必然會分裂爲兩個或兩個以上的子圖。
- 割邊集合:如果有一個邊集合,刪除這個邊集合以後,原圖變成多個連通塊,就稱這個點集爲割邊集合。
- 邊連通度:一個圖的邊連通度的定義爲,最小割邊集合中的邊數。
- 縮點:把沒有割邊的連通子圖縮爲一個點,此時滿足任意兩點之間都有兩條路徑可達。
注:求塊<>求縮點。縮點後變成一棵k個點k-1條割邊連接成的樹。而割點可以存在於多個塊中。 - 雙連通分量:分爲點雙連通和邊雙連通。它的標準定義爲:點連通度大於1的圖稱爲點雙連通圖,邊連通度大於1的圖稱爲邊雙連通圖。通俗地講,滿足任意兩點之間,能通過兩條或兩條以上沒有任何重複邊的路到達的圖稱爲雙連通圖。無向圖G的極大雙連通子圖稱爲雙連通分量。
複雜度
O(n+m)
有向圖
首先搜索一個點,賦 DFN 和 LOW 初值爲它的 DFN,把它丟進棧裏,然後
遍歷它的每個相鄰點:
if 如果該點沒被搜到過
then 搜索該點 然後用該點的 LOW 更新現在點的 LOW
else if 下個點還在棧裏
then 用下個點的 DFN/LOW 更新這個點的 LOW (爲什麼 DFN 和 LOW 都可以:因爲它在棧中說明它還不是強連通分量,所以 LOW 還沒被更新)
最後判斷一下它的 LOW 值和 DFN 值是否還相等,如果相等,說明有兩種情況:
1.它沒有出邊
2.它的子節點最終回到了它
兩種情況都說明以它爲根的樹爲一個強連通分量。這個時候就只要不斷退棧到這個點爲止,退出來的所有元素爲一個強連通分量。
Code
struct Tarjan{
static const int N=500010,M=500010;
int n,m,tot,_clock,scc;
int ne[M],to[M];bool in[N];
int fr[N],low[N],dfn[N],f[N],st[N];
inline void add(int u,int v){
to[++tot]=v;ne[tot]=fr[u];fr[u]=tot;
}
void Init(){
n=gi(),m=gi();
for(int i=1;i<=m;i++){
int u=gi(),v=gi();add(u,v);
}
}
inline void dfs(int x){
dfn[x]=low[x]=++_clock;
st[++tot]=x;in[x]=true;
for(int o=fr[x];o;o=ne[o])
if(!dfn[to[o]])dfs(to[o]),low[x]=min(low[x],low[to[o]]);
else if(in[to[o]])low[x]=min(low[x],low[to[o]]);
if(dfn[x]==low[x]){
scc++;
while(st[tot]!=x){
f[st[tot]]=scc;in[st[tot]]=false;tot--;
}
f[x]=scc;in[x]=false;tot--;
}
}
void Work(){
_clock=tot=scc=0;
memset(dfn,0,sizeof(dfn));
memset(in,false,sizeof(in));
for(int i=1;i<=n;i++)
if(!dfn[i])dfs(i);
}
}Tar;
縮點
上面代碼中f[]代表沒個點屬於那個塊,只需把每一塊縮成一個點即可,如果兩個塊中的點有相連,那麼將這個塊連起來
Code
//在上面結構體中加上這幾行就行
vector<int>p[N]
void Rebuild(){
for(int i=1;i<=n;i++)
for(int o=fr[i];o;o=ne[o]){
if(f[i]==f[to[o]])continue;
p[f[i]].push_back(f[to[o]]);
}
}
用途
縮完點非常好,dfs隨便跑,有很多方面應用,比如下面Practice的第一題
這個圖會變得非常優美,有向無環圖→DAG
無向圖
在無向圖中因爲一條邊可以來回走,所以要保證不能用父親更新,但是直接
記父親又不行,因爲可能會有重邊,所以就要記錄邊的編號。更新的過程還是一
樣 的 。
Articulation Point-割頂與連通度
在無向連通圖中,刪除一個頂點v及其相連的邊後,原圖從一個連通分量變成了兩個或多個連通分量,則稱頂點v爲割點,同時也稱關節點(Articulation Point)。一個沒有關節點的連通圖稱爲重連通圖(biconnected graph)。若在連通圖上至少刪去k 個頂點才能破壞圖的連通性,則稱此圖的連通度爲k。
關節點和重連通圖在實際中較多應用。顯然,一個表示通信網絡的圖的連通度越高,其系統越可靠,無論是哪一個站點出現故障或遭到外界破壞,都不影響系統的正常工作;又如,一個航空網若是重連通的,則當某條航線因天氣等某種原因關閉時,旅客仍可從別的航線繞道而行;再如,若將大規模的集成電路的關鍵線路設計成重連通的話,則在某些元件失效的情況下,整個片子的功能不受影響,反之,在戰爭中,若要摧毀敵方的運輸線,僅需破壞其運輸網中的關節點即可。
割頂求法
1. 對根節點u,若其有兩棵或兩棵以上的子樹,則該根結點u爲割點;
2. 若low[v]>=dfn[u],則u爲割點,u和它的子孫形成一個塊。因爲這說明u的子孫不能夠通過其他邊到達u的祖先,這樣去掉u之後,圖必然分裂爲兩個子圖。Analysis:對非葉子節點u(非根節點),若其子樹的節點均沒有指向u的祖先節點的回邊,說明刪除u之後,根結點與u的子樹的節點不再連通;則節點u爲割點。
Code
struct ArticulationPoint{
static const int N=500010,M=500010;
int n,m,tot,_clock;
int ne[M],to[M];bool cut[N];
int fr[N],low[N],dfn[N],father[N];
inline void add(int u,int v){
to[++tot]=v;ne[tot]=fr[u];fr[u]=tot;
}
void Init(){
n=gi(),m=gi();
for(int i=1;i<=m;i++){
int u=gi(),v=gi();add(u,v);add(v,u);
}
}
inline void Tarjan(int x,int fa){
dfn[x]=low[x]=++_clock;father[x]=fa;
for(int o=fr[x];o;o=ne[o])
if(!dfn[to[o]]){
Tarjan(to[o],x);
low[x]=min(low[x],low[to[o]]);
}
else if(to[o]>>1!=fa>>1)low[x]=min(low[x],low[to[o]]);
}
void Work(){
_clock=0;
memset(dfn,0,sizeof(dfn));
for(int i=1;i<=n;i++)
if(!dfn[i])Tarjan(i,-1);
tot=0;int k=0;//這裏1爲根節點
for(int i=2;i<=n;i++)
if(father[i]==1)tot++;
else if(low[i]>=dfn[father[i]])cut[father[i]]=1,++k;
if(tot>1)cut[1]=true,++k;
printf("%d\n",k);//割頂個數
for(int i=1;i<=n;i++)if(cut[i])printf("%d ",i);
}
}Tar;
Bridge-橋
橋 的 判 定 方 法 是 , 如 果 DFN(u) < LOW(v) 則 邊 (u,v) 爲 橋 。( 因 爲
DFN(u) < LOW(v)說明 v 沒有回到 u 之前的節點,即 u 和 v 不是一個塊(雙連通
分量,顧名思義要有兩條路)的。)
Code
struct Bridge{
static const int N=500010,M=500010;
int n,m,tot,_clock,ans;//ans 統計橋的個數
int ne[M],to[M];
int fr[N],low[N],dfn[N];
inline void add(int u,int v){
to[++tot]=v;ne[tot]=fr[u];fr[u]=tot;
}
void Init(){
n=gi(),m=gi();
for(int i=1;i<=m;i++){
int u=gi(),v=gi();add(u,v);add(v,u);
}
}
inline void Tarjan(int x,int fa){
dfn[x]=low[x]=++_clock;
for(int o=fr[x];o;o=ne[o])
if(!dfn[to[o]]){
Tarjan(to[o],x);
low[x]=min(low[x],low[to[o]]);
if(low[to[o]]>dfn[x])ans++;
}
else if(to[o]>>1!=fa>>1)low[x]=min(low[x],low[to[o]]);//重邊不爲橋
}
void Work(){
_clock=ans=0;
memset(dfn,0,sizeof(dfn));
for(int i=1;i<=n;i++)
if(!dfn[i])Tarjan(i,-1);
printf("%d",ans);
}
}Tar;
一些有用的定理
- 有向無環圖中唯一出度爲0的點,一定可以由任何點出發均可達(由於無環,所以從任何點出發往前走,必然終止於一個出度爲0的點)
- 有向無環圖中所有入度不爲0的點,一定可以由某個入度爲0的點出發可達。(由於無環,所以從任何入度不爲0的點往回走,必然終止於一個入度爲0的點)