演算法資料結構 | 三個步驟完成強連通分量分解的kosaraju演算法
我們來思考乙個問題,對於強連通分量分解的演算法來說,它的核心原理是什麼?
如果你看過我們之前的文章,那麼這個問題對你來說應該不難回答。既然是強連通分量,意味著分量當中每個點都可以互相連通。所以我們很容易可以想到,我們可以從乙個點出發,找到乙個迴路讓它再回到起點。這樣途中經過的點就都是強連通分量的一部分。
但是這樣會有乙個問題,就是需要保證強連通分量當中的每個點都被遍歷到,不能有遺漏。針對這個問題我們也可以想到解法,比如可以用搜尋演算法去搜尋它所有能夠達到的點和所有的路徑。但是這樣一來,我們又會遇到另外乙個問題。這個問題就是強連通分量之間的連通問題。
我們來看個例子:
在上面這張圖當中如果我們從點1出發,我們可以達到圖中的每乙個點。但是我們會發現1,2,3是乙個強連通分量,4,5,6是另外乙個。當我們尋找1所在的強連通分量的時候,很有可能會把4,5,6這三個點也帶進來。但問題是它們是自成分量的,並不應該算在1的強連通分量當中。
我們整理一下上面的分析和思路可以發現強連通分量分解這個演算法的核心其實就是解決這兩個問題,就是完備性問題。完備意味著不能遺漏也不能冗餘和錯誤,我們想明白核心問題所在之後就很容易搭建起思維框架,接下來我們再來看演算法的描述會容易理解得多。
tarjan演算法的第乙個機制是時間戳,也就是在遍歷的時候對每乙個遍歷到的點打上乙個值。這個值表示這是第幾個遍歷的元素。
stamp = 0
stamp_dict = {}
def dfs(u):
stamp_dict[u] = stamp
stamp += 1
for v in graph[u]:
dfs(v)
通過時間戳我們可以知道每個點被訪問的順序,這個順序是正向順序。舉個例子,比如說假設u和v兩個點,u的時間戳比v小。那麼它們之間的關係只有兩種可能,第一種是u能夠連通到v,說明從u到v的鏈路可以走通。第二種是u不能連通到v,這種情況不論反向的從v到u能否連通都不具有討論意義,因為它們一定不能互相連通。
所以我們想要找到連通的通路還需要找到反向的路徑,在kosaraju演算法當中我們是通過反向圖來實現的。在tarjan當中則採取了另外一種方法。因為我們已經知道各個點的時間戳了,我們完全可以通過時間戳來尋找反向的路徑。什麼意思呢?其實很簡單,當我們在遍歷u的時候如果遇到了乙個比u時間戳更小的v,那麼說明就存在一條反向的路徑從u通向v。如果v這時候還沒有出棧,意味著v是u的上游的話,那麼也就說明存在一條路徑從v通向u。這樣就說明了u和v可以互相連通。
既然找到了一對互相連通的u和v,那麼我們需要把它們記錄下來。但問題是我們怎麼知道記錄到什麼時候為止呢?這個邊界在**?tarjan演算法設計了另外乙個巧妙的機制解決了這個問題。
這個機制就是low機制,low[u]表示u這個點能夠連通到的所有的點的時間戳的最小值。時間戳越**明在搜尋樹當中的位置越高,也可以理解成u能夠連通到的處在搜尋樹中最高的點。那麼很明顯了,這個點就是u這個點所在強連通分量所在搜尋樹某一棵子樹的樹根。
這裡可能有一點點繞,我們再來看張圖:
圖中節點所在的序號就是遞迴遍歷的時間戳,我們可以發現對於圖上的每個點來說它們的low值都是1。很明顯1這個點在搜尋樹當中是2,3,4這三個點的祖先。也就是說這乙個強連通分量的遍歷是從1這個點開始的。當1這個點出棧的時候,意味著以1位樹根的子樹已經遍歷完了,所有可能存在的強連通分量也都已經找完了。
這就帶來了另外乙個問題,我們假設當前點是u,我們如何知道u這個點是否是圖中1這樣的樹根呢?有沒有什麼辦法可以標記出來呢?
當然是有的,這樣的點有乙個特性就是它們的時間戳等於它們的low。所以我們可以用乙個陣列維護找到的強連通分量,當這些強連通分量能夠遍歷到的樹根出棧的時候,把陣列清空。
scc =
stack =
def tarjan(u):
dfn[u], low[u] = stamp, stamp
stamp += 1
for v in graph[u]:
if not dfn[v]:
tarjan(v)
low[u] = min(low[u], low[v])
elif v in stack:
low[u] = min(low[u], dfn[v])
if dfn[u] == low[u]:
cur =
# 棧中u之後的元素是乙個完整的強連通分量
while true:
stack.pop()
if cur[-1] == u:
break
// cpp
int dfn[n], low[n], dfncnt, s[n], in_stack[n], tp;
int scc[n], sc; // 結點 i 所在 scc 的編號
int sz[n]; // 強連通 i 的大小
void tarjan(int u) else if (in_stack[v])
}if (dfn[u] == low[u])
scc[s[tp]] = sc;
sz[sc]++;
in_stack[s[tp]] = 0;
--tp;}}
最後,我們來看一下之前講過的經典例子:
首先我們從1點開始,一直深搜到6結束,當遍歷到6的時候,dfn[6]=4,low[6]=4,當6出棧時滿足條件,6獨立稱為乙個強連通分量。
同理,當5退出的時候也同樣滿足條件,我們得到了第二個強連通分量。
接著我們回溯到節點3,節點3還可以遍歷到節點4,4又可以連向1。由於1點已經在棧中,所以不會繼續遞迴1點,只會更新low[4] = 1,同樣當4退出的時候又會更新3,使得low[3] = 1。
最後我們返回節點1,通過節點1遍歷到節點2。2能連通的4點已經在棧中,並且dfn[4] > dfn[2],所以並不會更新2點。再次回到1點之後,1點沒有其他點可以連通,退出。退出的時候發現low[1] = dfn[1],此時棧中剩下的4個元素全部都是強連通分量。
到這裡,整個演算法流程的介紹就算是結束了,希望大家都可以enjoy今天的內容。
Tarjan演算法 學習筆記
tarjan演算法一般用於有向圖裡強連通分量的縮點。強連通分量 有向圖裡能夠互相到達的點的集合。大概是這麼個意思,自己意會 因為能夠互相到達,所以巨集觀上我們可以把它們看成乙個點,邊權也相應的加起來即可。下面是tarjan過程的 解釋 我們開兩個陣列,分別為dfn和low。dfn表示此點的時間戳,l...
Tarjan 演算法筆記
tarjan演算法 tarjan演算法屬於圖論中的乙個演算法,主要用來求乙個圖中的強連通分量,之後就可以做很多事,比如說縮點 求雙聯通分支等。強連通 在乙個有向圖中,對於幾個點,如果它們能夠互相到達,那麼稱它們強連通。強連通分量 可以這樣理解 把乙個圖里的點分成幾坨,每坨中的點都能夠互相到達 他們強...
tarjan演算法筆記
tarjan演算法可以用來求強連通塊的塊數,判斷條件是low x dfn x 注意點 縮點後是將強連通塊當作點,所以重建圖的時候,發現原圖2點不在同一強連通塊就可以按照原圖的方向鏈結2個超級點。建圖可以能有重邊但是大部分不會對解題產生影響。如果有需要可以用set去重。tarjan 當 low x d...