statement
這篇部落格引用了其他部落格的資料,我自己補充了一點。好文必須轉切補充。。
說到以Tarjan命名的演算法,我們經常提到的有3個,其中就包括本文所介紹的求強連通分量的Tarjan演算法。而提出此演算法的普林斯頓大學的Robert E Tarjan教授也是1986年的圖靈獎獲得者(具體原因請看本博“曆屆圖靈獎得主”一文)。 首先明確幾個概念
補充:
1.割點:若刪掉某點後,原連通圖分裂為多個子圖,則稱該點為割點。
2.割點集合:在一個無向連通圖中,如果有一個頂點集合,刪除這個頂點集合,以及這個集合中所有頂點相關聯的邊以後,原圖變成多個連通塊,就稱這個點集為割點集合。
3.點連通度:最小割點集合中的頂點數。
4.割邊(橋):刪掉它之後,圖必然會分裂為兩個或兩個以上的子圖。
5.割邊集合:如果有一個邊集合,刪除這個邊集合以後,原圖變成多個連通塊,就稱這個點集為割邊集合。
6.邊連通度:一個圖的邊連通度的定義為,最小割邊集合中的邊數。
7.縮點:把沒有割邊的連通子圖縮為一個點,此時滿足任意兩點之間都有兩條路徑可達。 註:求塊<>求縮點。縮點後變成一棵k個點k-1條割邊串連成的樹。而割點可以存在於多個塊中。
8.雙連通分量:分為點雙連通和邊雙連通。它的標準定義為:點連通度大於1的圖稱為點雙連通圖,邊連通度大於1的圖稱為邊雙連通圖。通俗地講,滿足任意兩點之間,能通過兩條或兩條以上沒有任何重複邊的路到達的圖稱為雙連通圖。無向圖G的極大雙連通子圖稱為雙連通分量。
詳解:
1、強連通圖。在一個強連通圖中,任意兩個點都通過一定路徑互相連通。比如圖一是一個強連通圖,而圖二不是。因為沒有一條路使得點4到達點1、2或3。
2、強連通分量。在一個非強連通圖中極大的強連通子圖就是該圖的強連通分量。比如圖三中子圖{1,2,3,5}是一個強連通分量,子圖{4}是一個強連通分量。
Tarjan演算法的應用論述:
1.求強連通分量、割點、橋、縮點:
對於Tarjan演算法中,我們得到了dfn和low兩個數組,
low[u]:=min(low[u],dfn[v])——(u,v)為後向邊,v不是u的子樹; low[u]:=min(low[u],low[v])——(u,v)為樹枝邊,v為u的子樹;
下邊對其進行討論:
若low[v]>=dfn[u],則u為割點,u和它的子孫形成一個塊。
因為這說明u的子孫不能夠通過其他邊到達u的祖先,這樣去掉u之後,圖必然分裂為兩個子圖。
若low[v]>dfn[u],則(u,v)為割邊。理由類似於上一種情況。
其實,tarjan演算法的基礎是DFS。我們準備兩個數組Low和Dfn**。Low數組是一個標記數組,記錄該點所在的強連通子圖所在搜尋子樹的根節點的Dfn值(很繞嘴,往下看你就會明白),**Dfn數組記錄搜尋到該點的時間,也就是第幾個搜尋這個點的。根據以下幾條規則,經過搜尋遍曆該圖(無需回溯)和對棧的操作,我們就可以得到該有向圖的強連通分量。
數組的初始化:當首次搜尋到點p時,Dfn與Low數組的值都為到該點的時間。
堆棧:每搜尋到一個點,將它壓入棧頂。
當點p有與點p’相連時,如果此時(時間為dfn[p]時)p’不在棧中,p的low值為兩點的low值中較小的一個。
當點p有與點p’相連時,如果此時(時間為dfn[p]時)p’在棧中,p的low值為p的low值和p’的dfn值中較小的一個。
每當搜尋到一個點經過以上操作後(也就是子樹已經全部遍曆)的low值等於dfn值,則將它以及在它之上的元素彈出棧。這些出棧的元素組成一個強連通分量。
繼續搜尋(或許會更換搜尋的起點,因為整個有向圖可能分為兩個不連通的部分),直到所有點被遍曆。
由於每個頂點只訪問過一次,每條邊也只訪問過一次,我們就可以在O(n+m)的時間內求出有向圖的強連通分量。但是,這麼做的原因是什麼呢。 Tarjan演算法的操作原理如下:
Tarjan演算法基於定理:在任何深度優先搜尋中,同一強連通分量內的所有頂點均在同一棵深度優先搜尋樹中。也就是說,強連通分量一定是有向圖的某個深搜樹子樹。
可以證明,當一個點既是強連通子圖Ⅰ中的點,又是強連通子圖Ⅱ中的點,則它是強連通子圖Ⅰ∪Ⅱ中的點。
這樣,我們用low值記錄該點所在強連通子圖對應的搜尋子樹的根節點的Dfn值。注意,該子樹中的元素在棧中一定是相鄰的,且根節點在棧中一定位於所有子樹元素的最下方。
強連通分量是由若干個環組成的。所以,當有環形成時(也就是搜尋的下一個點已在棧中),我們將這一條路徑的low值統一,即這條路徑上的點屬於同一個強連通分量。
如果遍曆完整個搜尋樹後某個點的dfn值等於low值,則它是該搜尋子樹的根。這時,它以上(包括它自己)一直到棧頂的所有元素組成一個強連通分量。
Tarjan求有向圖強連通分量、割點、割邊的代碼: Var n,m,i,j,x,y,z:longint; a,b:array[0..1000,0..1000]of longint;//圖 dfn,low,s:array[0..1000]of longint;//dfn為時間戳記,low為祖先,s為棧 vis,ins:array[0..1000]of boolean;//vis為是否訪問,ins為是否在棧中 num,p:longint; function min(x,y:longint):longint; begin if x<y then exit(x) else exit(y); end; procedure tarjan(u:longint); var i,v:longint; begin inc(num);//給定一個時間戳記 dfn[u]:=num; low[u]:=num; vis[u]:=true; inc(p);//入棧 s[p]:=u; ins[u]:=true; for i:=1 to b[u,0] do//注意只有u與i相連才進行下面的操作 if not vis[b[u,i]] then//未被訪問 begin tarjan(b[u,i]); low[u]:=min(low[u],low[b[u,i]]);//是樹枝邊,取兩個low的min值 {如果是求割點或者割邊,在這裡判斷dfn[u]和low[v]的大小並進行彈棧即可。} end else if ins[b[u,i]] then//在棧中 low[u]:=min(low[u],dfn[b[u,i]]);//非樹枝邊,取low與dfn的min值 if dfn[u]=low[u] then//已經找到一個強連通分量,彈棧。 repeat v:=s[p]; write(v,' '); ins[v]:=false; dec(p); if u=v then writeln; until u=v; end; begin readln(n,m); for i:=1 to m do//構圖 begin readln(x,y); inc(b[x,0]); b[x,b[x,0]]:=y; end; tarjan(1); End.
2.求雙連通分量以及構造雙連通分量:
對於點雙連通分支,實際上在求割點的過程中就能順便把每個點雙連通分支求出。
建立一個棧,儲存當前雙連通分支,在搜尋圖時,每找到一條樹枝邊或後向邊(非橫叉邊),就把這條邊加入棧中。如果遇到某時滿足DFS(u)<=Low(v),說明u是一個割點,同時把邊從棧頂一個個取出,直到遇到了邊(u,v),取出的這些邊與其關聯的點,組成一個點雙連通分支。
割點可以屬於多個點雙連通分支,其餘點和每條邊只屬於且屬於一個點雙連通分支。
對於邊雙連通分支,求法更為簡單。只需在求出所有的橋以後,把橋邊刪除,原圖變成了多個連通塊,則每個連通塊就是一個邊雙連通分支。橋不屬於任何一個邊雙連通分支,其餘的邊和每個頂點都屬於且只屬於一個邊雙連通分支。
一個有橋的連通圖,如何把它通過加邊變成邊雙連通圖。
方法為首先求出所有的橋,然後刪除這些橋邊,剩下的每個連通塊都是一個雙連通子圖。把每個雙連通子圖收縮為一個頂點,再把橋邊加回來,最後的這個圖一定是一棵樹,邊連通度為1。
統計出樹中度為1的節點的個數,即為分葉節點的個數,記為leaf。
則至少在樹上添加(leaf+1)/2條邊,就能使樹達到邊二連通,所以至少添加的邊數就是(leaf+1)/2。
具體方法為,首先把兩個最近公用祖先最遠的兩個分葉節點之間串連一條邊,這樣可以把這兩個點到祖先的路徑上所有點收縮到一起,因為一個形成的環一定是雙連通的。然後再找兩個最近公用祖先最遠的兩個分葉節點,這樣一對一對找完,恰好是(leaf+1)/2次,把所有點收縮到了一起。
3.求最近公用祖先(LCA)
在遍曆到u時,先tarjan遍曆完u的子樹,則u和u的子樹中的節點的最近公用祖先就是u,並且u和【u的兄弟節點及其子樹】的最近公用祖先就是u的父親。注意到由於我們是按照DFS順序遍曆的,我們可用一個color數組標記,正在訪問的染色為1,未訪問的標記為0,已經訪問到即在【u的子樹中的】及【u的已訪問的兄弟節點及其子樹中的】染色標記為2,這樣我們可以通過並查集的不斷合并更新,通過find實現以上目標。 Code
function find(x:longint):longint; begin if f[x]<>x then f[x]:=find(f[x]); end; procedure tarjan(u:longint); begin f[u]:=u; for i:=1 to n do if (g[u,i])and(color[i]=0) then//g[u,i]表示u連著i begin f[i]:=u; end; for i:=1 to n do if ((ask[u,i])or(ask[i,u]))and(color[i]=2) then//ask[u,i]表示詢問了u,i begin lca[u,i]:=find(i); lca[i,u]:=lca[u,i]; end; color[u]:=2; end;
一道例題(題號我忘了囧) CODE
#include <cstdio>#include <iostream>#define N 50001#define LL long long#define fo(i,a,b) for (int i=a;i<=b;i++)using namespace std;int dfn[N],low[N],zhan[N];bool v[N];int t[2*N],next[2*N],last[2*N];int n,m,l,top=0;LL ans=0;void add(int x,int y){ t[++l]=y; next[l]=last[x]; last[x]=l;}void tarjan(int x){ zhan[++top]=x; dfn[x]=++l; low[x]=l; int k=last[x]; // bz[x]=true; v[x]=true; while (k!=0) { if (dfn[t[k]]==0) { tarjan(t[k]); if (low[t[k]]<low[x]) low[x]=low[t[k]]; } else if (v[t[k]] && dfn[t[k]]<low[x]) low[x]=dfn[t[k]]; k=next[k]; } if (low[x]==dfn[x]) { LL xdl=0; while (zhan[top+1]!=x) { v[zhan[top]]=false; top--; xdl++; } ans+=(xdl-1)*xdl/2; }}int main(){ scanf("%d%d",&n,&m); int x,y; fo(i,1,m) { scanf("%d%d",&x,&y); add(x,y); } l=0; fo(i,1,n) if (not bz[i]) tarjan(i); printf("%d",ans);}