1. 程式人生 > >tarjan演算法求scc & 縮點

tarjan演算法求scc & 縮點

##前置知識 圖的遍歷(dfs) ##強連通&強連通分量 對於有向圖G中的任意兩個頂點u和v存在u->v的一條路徑,同時也存在v->u的路徑,我們則稱這兩個頂點強連通。以此類推,強連通分量就是某一個分量內各個頂點之間互相連通。 簡單來說,就是有向圖內的一個分量,其中的任意兩個點之家可以互相到達。 求有向圖內部強連通分量的方法大概有2種:tarjan演算法,korasaju演算法。這裡我們只對tarjan演算法進行討論。 ##tarjan演算法 tarjan演算法是tarjan神仙提出的基於dfs時間戳和堆疊的演算法,這裡我們可以先來看一下什麼是dfs時間戳 ###dfs時間戳 dfs時間戳就是dfs的先後順序,詳細來講,比如我們dfs最先訪問到的節點是A,於是A的時間戳就是1,第二個訪問到的節點是E,那麼E的時間戳就是2,我們用$dfn[u]$來表示u節點的時間戳,應該算是比較簡單的 ###演算法步驟 首先,除了dfn以外我們還需要一個low陣列,這個陣列記錄了某個點通過圖上的邊能回溯到的dfn值最小的節點。這句話相信在大多數部落格裡面都有提到,這裡我們來看一個簡單的例子: 首先,我們有一個圖G: ![](https://img2020.cnblogs.com/blog/1944344/202006/1944344-20200621162613976-1417950448.png) 假設我們從a點出發開始dfs,我們可以畫出一個dfs樹: ![](https://img2020.cnblogs.com/blog/1944344/202006/1944344-20200621162614232-699865144.png) 為什麼我們畫出來的dfs樹和原來的圖不一樣呢?因為我們在dfs的過程中實際上是會忽略某一些連線到已訪問節點的邊的,這些邊我們暫且稱之為回邊。對於點u來說,$low[u]$儲存的就是點u通過某一條(或者是幾條)回邊能到達的dfn值最小的節點(也就是被最先訪問的節點)。假設這個dfn值最小的節點是u',我們可以知道,因為u和u'都是在一棵dfs樹上的,並且u'可以到達u,同時u可以通過一條或多條回邊到達u',也就是說u'->u路徑上的任意節點都可以通過這一條回邊來互相到達,也就是說他們會形成一個強連通分量。 ###更加詳細的例子 我們有一個新圖G: ![](https://img2020.cnblogs.com/blog/1944344/202006/1944344-20200621162614412-406790751.jpg) 假設我們從A點出發開始dfs,一路跑到D點,那麼我們為這個圖上的每一個點加上dfn陣列和low陣列的值(dfn,low),整個圖就會長成這個樣子: ![](https://img2020.cnblogs.com/blog/1944344/202006/1944344-20200621162614608-599873688.jpg) 此時我們會遇到一條D->A的回邊,也就是說點D能訪問到的dfn值最小的節點從點D本身變化到了A點,所以點D的low值就會發生相應的變化,$low[D]=min(low[D],dfn[A])$。 緊接著,dfs發生回溯,我們沿著之前的路徑逐步更新路徑上節點的low值,於是就有$low[C]=min(low[C],low[D])$,直到更新到某一個dfn值和low值相同的節點。因為這個節點能訪問到的最小dfn的節點就是其本身,也就是說這個節點是整個scc最先被訪問到的節點。 全部搞完大概會變成這個樣子: ![](https://img2020.cnblogs.com/blog/1944344/202006/1944344-20200621162614791-1914292667.jpg) 我們用一個輔助棧來儲存dfs的路徑,這樣就可以在找到一個強連通分量裡面最早被訪問到的節點的時候可以輸出路徑。同時因為dfs訪問是一條路走到黑的,所以可以保證棧內在節點u(low[u]==dfn[u])之前的的節點都是屬於同一個scc的。 還是上面這幅圖,我們順便把E點給更新了: ![](https://img2020.cnblogs.com/blog/1944344/202006/1944344-20200621162614984-1360072650.jpg) 跑完E點之後就會發現,E點本身的low就是和dfn相等的,所以此時棧內也只有E這一個節點。 於是上面這個圖的scc有以下幾個: [E] [A,B,C,D] ##程式碼實現 首先我們要發現,在dfs的初期我們每一個節點的low和dfn都是相同的,也就是說有dfn[u]=low[u]=++cnt(cnt為計數變數),並且在回溯的過程中要用後訪問節點的low值來更新先訪問節點的low值,也就是說有$low[u]=min(low[u],low[v])$,當訪問到某一個在棧中的節點的時候,我們要用這個節點的dfn值來更新其他節點,所以有$low[u]=min(low[u],dfn[v])$。 那麼我們一個簡單的程式碼就可以寫出來了: ~~~c++ void tarjan(int u){ dfn[u]=low[u]=++cnt; s.push(u); ins[u]=1; for(i