Robert Tarjan,一個很牛逼的計算機科學家。
tarjan算法真的是一個神奇的算法,一個簡單的dfs卻可以解決連通性的問題以及求最近公共祖先。
1.求強連通分量
首先介紹一下什麼是強連通分量。
強連通(Strongly Connected)是指一個有向圖(Directed Graph)中任意兩點v1、v2間存在v1到v2的路徑(path)及v2到v1的路徑。
強連通分量:在有向圖G中,如果兩個頂點vi,vj間(vi>vj)有一條從vi到vj的有向路徑,同時還有一條從vj到vi的有向路徑,則稱兩個頂點強連通(strongly connected)。如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖。有向圖的極大強連通子圖,稱爲強連通分量(strongly connected components)。
當我們知道了強連通分量是什麼了之後,我們來學習一下tarjan算法是如何求強連通分量的。
其實tarjan算法寫起來很簡單,就是一遍dfs,但是理解起來確實有點麻煩。
首先,
dfs就是我們通常的dfs,每個點需要標記有沒有訪問過,如果沒有就往下dfs。
dfs出來的其實是一個搜索樹。
每個節點我們需要保存兩個信息
1.dfn[x],表示x是第幾個訪問的,就相當於有個cnt,每調用一層dfs,就使得cnt+1,然後賦值給當前訪問的節點。
2.low[x],這個就不太好理解了,指的是,從這個點往下繼續搜索所能搜索到的反回邊所指向的已經搜過的節點的最小編號。
這個low[x]其實是在dfs的過程中遞歸求出來的一個結果,對於u->v分兩種情況
(0.初始時low[u]=dfn[u],如果這個點處理完low[u]仍然不能改變的話,說明無法搜索到已訪問過的點)
1.v是沒有訪問過的節點,dfs(v),回溯時low[u]=min(low[u],low[v]);
2.v是已經訪問過的節點,如果該點還沒有被劃分到某一個強連通分量裏,low[u]=min(low[u],dfn[v]);
第2種情況可以理解爲維護low的遞歸邊界,因爲找到了已訪問過的節點,否則就遞歸下去並用low[v]去更新low[u];
其次,
我們在dfs的時候還需要維護一個棧,每訪問到一個點就將其進棧,這個棧其實就是用來存在同一個強連通分量裏的點。
那麼什麼時候出棧呢?
當一個點處理完後,low[u]==dfn[u]的時候。之前說過了,low[u]==dfn[u]表明這個點無法搜索到已經訪問過的點,那麼當前棧內,從這個點往上的所有點就都屬於這個強連通分量,並將它們出棧。
怎麼理解呢?
如果low[u]==dfn[u],那麼說明從u點往後搜索的點沒有能搜到在u點之前訪問的點,那麼他們一定不會屬於更往前的點組成的強連通分量。有人又問了,這樣也未必屬於u所在的強連通分量啊。可是如果這個點不屬於u所在的強連通分量,也不屬於u之前訪問的點的強連通分量,那麼它一定已經和自己所在的強連通分量的所有點一起出棧了,所以棧中u往上的所有點就都是u所在的強連通分量。
代碼實現
vector <int> a[maxn];
int index=0,cnt=0;
int low[maxn],dfn[maxn],vis[maxn],id[maxn];
stack <int> s;
void tarjan(int x)
{
index++;
low[x]=dfn[x]=index;
s.push(x);
vis[x]=1;
int l=a[x].size();
for(int i=0;i<l;i++)
{
int t=a[x][i];
if(dfn[t]==0)
{
tarjan(t);
low[x]=min(low[x],low[t]);
}
else
if(id[t]!=0)
low[x]=min(low[x],dfn[t]);
}
if(low[x]==dfn[x])
{
cnt++;
while(!s.empty())
{
int t=s.top();
s.pop();
id[t]=cnt;
if(t==x)
break;
}
}
}
2.tarjan求割點
理解了dfn和low的意思之後,tarjan求割點就很好理解了。
先來看割點的定義:
在一個無向圖中,如果有一個頂點集合,刪除這個頂點集合以及這個集合中所有頂點相關聯的邊以後,圖的連通分量增多,就稱這個點集爲割點集合。
如果某個割點集合只含有一個頂點X(也即{X}是一個割點集合),那麼X稱爲一個割點。
換句話說就是在一個無向連通圖中,如果某一個點被去掉之後使得剩下的圖不再連通,那麼這個點就是割點,它把圖分割了。
求割點就是在上面所展示的過程中,找到某個點u,如果dfn[u]<=low[u],那麼這個節點u就是割點。
爲什麼?
dfn[u]<=low[u]表示u點往下搜索無法搜索到更往前訪問過的點。
如果無法搜索到之前的點,那麼就表示把這個點拿掉,一個連通圖就被分成了兩個或多個連通圖。
我們可以通過下圖直觀地感受一下。
如圖所示是割點最基本的表現形式。
可以看出
圖中紅色部分與藍色部分與頂點t組成了一個連通圖,而兩部分要想到達彼此,必須經過t點。所以當我們把t點去掉時,一個連通圖就變成了兩個,即紅色部分與藍色部分。
所以t點就是一個割點。
我們會發現,無論我們搜索順序是 紅->t->藍 還是 藍->t->紅,我們都無法從t點往後搜索到t點往前的點,因爲必然要兩次經過t點,而一個點我們只走一次,所以dfn[u]<=low[u].
3.tarjan求橋
tarjan求橋和求割點是差不多的,在dfs的時候,對於任意一條邊u->v,如果dfn[u]