RobertTarjan真的是一個傳說級的大人物。
他發明的LCT,SplayTree這些數據結構真的給我帶來了諸多便利,各種動態圖論題都可以用LCT解決。
而且,Tarjan並不只發明了LCT,他對計算機科學做出的貢獻真的很多。
這一篇我就來以他名字命名的Tarjan算法可以O(n)求出無向圖的割點和橋。
進一步可以求出無向圖的DCC( 雙連通分量 )。不止無向圖,Tarjan算法還可以求出有向圖的SCC( 強連通分量 )。
Tarjan算法基於dfs,接下來我們引入幾個基本概念。
dfn:時間戳
我們對一張圖進行深度優先遍歷,根據第一次訪問到它的時間順序給它打上一個標記,這個標記就是時間戳。
搜索樹:
在一張無向連通圖中選定任意一個節點進行深度優先遍歷,每個點僅訪問一次。所有發生了遞歸的邊會構成一棵樹,我們稱其為無向連通圖的“搜索樹”。
追溯值:
除了時間戳,Tarjan算法還引入了另一個概念:“追溯值” low。
我們用subtree(x)表示搜索樹中以x為根的子樹,low[x]定義為下列節點的時間戳的最小值:
1. subtree(x)中的節點 2. 通過一條不在搜索樹上的邊,能夠到達subtree(x)中的節點
我們來畫一個圖理解一下:(方便起見,圖中的節點編號就是它的時間戳)

圖中紅色的邊就是這張圖的搜索樹

那么我們容易得出:subtree(2)={4,3},5可以通過不在搜索樹上的邊到達subtree(2)。
所以,low[2]=min{dfn[4],dfn[3],dfn[5]},得出low[2]=3。
根據定義來計算low[x]的方法就非常明顯了。因為subtree(x)包括x,所以先令low[x]=dfn[x]。
然后遍歷從x出發的每一條邊(x,y),計算low[x]。
接下來給出無向圖的橋和割點判定法則。
無向邊(x,y)是橋,當且僅當x在搜索樹上的一個子節點y滿足low[y]>dfn[x]。
若x不是搜索樹的根節點,則x是割點當且僅當搜索樹上的一個子節點y滿足low[y]>=dfn[x]。
若x是根節點,則x是割點當且僅當搜索樹上存在至少兩個x的子節點y1,y2滿足上式。
橋邊有以下性質:
1. 橋一定是搜索樹中的邊 2. 一個簡單環中邊都不是橋邊
一個環被稱為簡單環當且僅當其包含的所有點都只在這個環中被經過了一次。
擴展內容:這里給出用dfs求出一個圖中所有簡單環的代碼
int cnt; void dfs(int u){ dfn[u]=++cnt; for(int i=head[u];i;i=nxt(i)){ int v=to(i); if(v==fa[u])continue; if(!dfn[v])fa[v]=u,dfs(v); else if(dfn[u]<dfn[v]){ printf("%d",v); do{ printf(" %d",fa[v]);v=fa[v]; }while(v!=u); //找完一個環了 putchar('\n'); } } }
這個作為擴展內容就不再展開敘述。
下面給出求出無向圖中所有的橋的代碼:
#include<bits/stdc++.h> #define N 100010 using namespace std; inline int read(){ int data=0,w=1;char ch=0; while(ch!='-' && (ch<'0'||ch>'9'))ch=getchar(); if(ch=='-')w=-1,ch=getchar(); while(ch>='0' && ch<='9')data=data*10+ch-'0',ch=getchar(); return data*w; } struct Edge{ int nxt,to; #define nxt(x) e[x].nxt #define to(x) e[x].to }e[N<<1]; int dfn[N],low[N],tot=1;//儲存邊的編號,由於要用^1找反向邊,從1開始 int bridge[N],head[N]; int n,m,cnt; void addedge(int f,int t){ nxt(++tot)=head[f];to(tot)=t;head[f]=tot; } void tarjan(int x,int in_edge){//in_edge表示遞歸進入每個節點的邊的編號 dfn[x]=low[x]=++cnt; for(int i=head[x];i;i=nxt(i)){ int y=to(i); if(!dfn[y]){ tarjan(y,i); low[x]=min(low[x],low[y]);//在搜索樹上的邊 if(low[y]>dfn[x])//橋判定法則 bridge[i]=bridge[i^1]=1;//這條邊和它的反向邊都是橋 }else if(i!=(in_edge^1)) low[x]=min(low[x],dfn[y]);//不在搜索樹上的邊 } } int main(){ n=read();m=read(); for(int i=1;i<=m;i++){ int x=read(),y=read(); addedge(x,y);addedge(y,x); } for(int i=1;i<=n;i++) if(!dfn[i])tarjan(i,0); for(int i=2;i<tot;i+=2) if(bridge[i]) printf("%d %d\n",to(i^1),to(i));//輸出橋兩邊的點 }
以上就是求無向圖中所有橋的程序了,可以自己畫圖模擬一下tarjan算法的流程加深理解。
下面給出求無向圖中所有割點的程序:
這里需要注意的是,由於割點判定法則是小於等於號,所以不需要考慮父節點和重邊的問題,所有dfn[x]都可以用來更新low[x]
#include<bits/stdc++.h> #define N 100010 using namespace std; inline int read(){ int data=0,w=1;char ch=0; while(ch!='-' && (ch<'0'||ch>'9'))ch=getchar(); if(ch=='-')w=-1,ch=getchar(); while(ch>='0' && ch<='9')data=data*10+ch-'0',ch=getchar(); return data*w; } struct Edge{ int nxt,to; #define nxt(x) e[x].nxt #define to(x) e[x].to }e[N<<1]; int head[N],dfn[N],low[N],rt,tot=1,n,m,cnt; int cut[N]; inline void addedge(int f,int t){ nxt(++tot)=head[f];to(tot)=t;head[f]=tot; } void tarjan(int x){ dfn[x]=low[x]=++cnt; int flag=0; for(int i=head[x];i;i=nxt(i)){ int y=to(i); if(!dfn[y]){ tarjan(y); low[x]=min(low[x],low[y]); if(low[y]>=dfn[x]){//就一個割點判斷法則沒必要解釋什么了吧? flag++; if(x!=rt||flag>1)cut[x]=1; } }else low[x]=min(low[x],dfn[y]); } } int main(){ n=read();m=read(); for(int i=1;i<=m;i++){ int x=read(),y=read(); if(x==y)continue; //自環直接判掉好吧,不多bb addedge(x,y);addedge(y,x); } for(int i=1;i<=n;i++) if(!dfn[i])rt=i,tarjan(i);//無向圖不一定連通,對每一個連通塊都要跑一發tarjan for(int i=1;i<=n;i++) if(cut[i])printf("%d ",i); return 0; }
橋邊判定法則和割點判定法則后面會update上,這一篇暫時更到這里,下一篇講e-DCC和v-DCC的求法
主要是聯賽在即,更新盡量多的算法扎實自己基礎才要緊些...請多見諒
