1. 程式人生 > 其它 >AcWing 1174. 受歡迎的牛

AcWing 1174. 受歡迎的牛

題目傳送門

一、有向圖強連通分量

在有向圖\(G\)中,如果兩個頂點間至少存在一條互相可達的路徑,稱兩個頂點強連通(\(strongly\) \(connected\))。如果有向圖\(G\)的每兩個頂點都強連通,稱\(G\)是一個強連通圖。非強連通圖有向圖的極大強連通子圖,稱為強連通分量(\(strongly\) \(connected\) \(components\))。

下圖中,子圖\(\{1,2,3,4\}\)為一個強連通分量,因為頂點\(1,2,3,4\)兩兩可達。\(\{5\},\{6\}\)也分別是兩個強連通分量。

直接根據定義,用雙向遍歷交集的方法求強連通分量,時間複雜度為\(O(N^2+M)\)

。更好的方法是\(Kosaraju\)演算法或\(Tarjan\)演算法,兩者的時間複雜度都是\(O(N+M)\)。本文介紹的是\(Tarjan\)演算法。

二、Tarjan演算法

\(Tarjan\)演算法是基於對圖深度優先搜尋的演算法,每個強連通分量為搜尋樹中的一棵子樹。搜尋時,把當前搜尋樹中未處理的節點加入一個堆疊,回溯時可以判斷棧頂到棧中的節點是否為一個強連通分量。

定義\(DFN(u)\)為節點\(u\)搜尋的次序編號(時間戳),\(Low(u)\)\(u\)\(u\)的子樹能夠追溯到的最早的棧中節點的次序編號。由定義可以得出:

Low(u)=Min{
    DFN(u),   當前節點ID號
    Low(v),  (u,v)為樹枝邊,u為v的父節點 (如果我的孩子能走到更小的號,就是我能走到的最小號)
    DFN(v),  (u,v)為指向棧中節點的後向邊(非橫叉邊)
}

\(DFN(u)=Low(u)\)時,以\(u\)為根的搜尋子樹上所有節點是一個強連通分量。
演算法虛擬碼如下:

tarjan(u){
	DFN[u]=Low[u]=++Index                      // 為節點u設定次序編號和Low初值
	Stack.push(u)                              // 將節點u壓入棧中
	for each (u, v) in E                       // 列舉每一條邊
		if (v is not visted)               // 如果節點v未被訪問過
			tarjan(v)                  // 繼續向下找
			Low[u] = min(Low[u], Low[v])
		else if (v in S)                   // 如果節點v還在棧內
			Low[u] = min(Low[u], DFN[v])
	
        if (DFN[u] == Low[u])                      // 如果節點u是強連通分量的根
		repeat
			v = S.pop                  // 將v退棧,為該強連通分量中一個頂點
			print v
		until (u== v)
}

接下來是對演算法流程的演示。

從節點\(1\)開始\(DFS\),把遍歷到的節點加入棧中。搜尋到節點\(u=6\)時,\(DFN[6]=LOW[6]\),找到了一個強連通分量。退棧到\(u=v\)為止,{\(6\)}為一個強連通分量。

返回節點\(5\),發現\(DFN[5]=LOW[5]\),退棧後{\(5\)}為一個強連通分量。

返回節點\(3\),繼續搜尋到節點\(4\),把\(4\)加入堆疊。發現節點\(4\)向節點\(1\)有後向邊,節點\(1\)還在棧中,所以\(LOW[4]=1\)。節點\(6\)已經出棧,\((4,6)\)是橫叉邊,返回\(3\)\((3,4)\)為樹枝邊,所以\(LOW[3]=LOW[4]=1\)

繼續回到節點\(1\),最後訪問節點\(2\)。訪問邊\((2,4)\)\(4\)還在棧中,所以\(LOW[2]=DFN[4]=5\)。返回\(1\)後,發現\(DFN[1]=LOW[1]\),把棧中節點全部取出,組成一個連通分量{\(1,3,4,2\)}。

至此,演算法結束。經過該演算法,求出了圖中全部的三個強連通分量{\(1,3,4,2\)},{\(5\)},{\(6\)}。

可以發現,執行\(Tarjan\)演算法的過程中,每個頂點都被訪問了一次,且只進出了一次堆疊,每條邊也只被訪問了一次,所以該演算法的時間複雜度為\(O(N+M)\)

求有向圖的強連通分量還有一個強有力的演算法,為\(Kosaraju\)演算法。\(Kosaraju\)是基於對有向圖及其逆圖兩次\(DFS\)的方法,其時間複雜度也是\(O(N+M)\)。與\(Trajan\)演算法相比,\(Kosaraju\)演算法可能會稍微更直觀一些。但是\(Tarjan\)只用對原圖進行一次\(DFS\),不用建立逆圖,更簡潔。在實際的測試中,\(Tarjan\)演算法的執行效率也比\(Kosaraju\)演算法高\(30\)%左右。此外,該\(Tarjan\)演算法與求無向圖的雙連通分量(割點、橋)的\(Tarjan\)演算法也有著很深的聯絡。學習該\(Tarjan\)演算法,也有助於深入理解求雙連通分量的\(Tarjan\)演算法,兩者可以類比、組合理解。

求有向圖的強連通分量的\(Tarjan\)演算法是以其發明者\(Robert\) \(Tarjan\)命名的。\(Robert\) \(Tarjan\)還發明瞭求雙連通分量的\(Tarjan\)演算法,以及求最近公共祖先的離線\(Tarjan\)演算法,在此對\(Tarjan\)表示崇高的敬意。

網路大神的完美圖解,附上以備複習之用: