從頭開始學習OI之Tarjan. 今天從新學習了Tarjan算法..來這裏寫一下學習筆記...node
Tarjan算法,是一個關於圖的聯通性的神奇算法.基於DFS(深度優先搜索).是對於有向圖的算法是.根據樹,棧,打標記等方法來完成剖析一個圖的工做.算法
咱們先來學習一下Tarjan算法須要知道的定義:
強連通,強連通圖,強連通份量數組
在一個有向圖 G 裏,有兩個點 a,b ,由a有一條路能夠走到b,由b又有一條路能夠走到a,咱們就叫這兩個頂點 (a,b) 強連通.學習
若是在一個有向圖 G 中,每兩個點都強連通,咱們就叫這個圖 強連通圖。spa
在一個有向圖 G 中,有一個子圖,這個子圖每2個點都知足強連通,咱們就叫這個子圖叫作 強連通份量 (如圖中的 1,2,3 組成的份量就叫強連通份量)
code
Tarjan所作的事情很簡單...就是找到每個強連通份量(單獨一個點也算是強連通份量..) 在實現算法以前...咱們先定義幾個數組:
dfn[i] 表示第 i 個節點的時間戳..也就是在DFS這張圖的時候這個點被遍歷到的時刻..
low[i] 表示第 i 個節點所在的強連通份量的根節點(按理說強連通份量無所謂根節點..這裏的根節點是指在那一棵DFS樹中這個強連通份量的根節點)component
很明顯若是 dfn[i] == low[i] 那麼就表明這一個點是一個強連通份量的根
先看一下算法流程blog
void Tarjan(int u) { dfn[u] = low[u] = ++Index; //將dfn和low都初始化爲時間戳,也就是dfs到的時刻 Stack[++top] = u; //將該點壓入dfs棧中 vis[u] = 1; //標記點在棧中 for(int i = head[u]; i; i = edge[i].next) { //DFS過程 if(!dfn[edge[i].to]) { //若是該點沒有被搜索到過 Tarjan(edge[i].to); //對於該點進行算法(即dfs的過程) low[u] = min(low[u],low[edge[i].to]); //搜索完成返回時更新一下這個強連通份量裏的全部點的在dfs樹中的根節點 } else if(vis[edge[i].to]) { //若是該點已經在搜索棧中,那麼表明當前棧中在這個點後的點在一個強連通份量裏,那麼這個搜索到的點就是這個強連通份量的根節點.. low[u] = min(low[u],dfn[edge[i].to]); //將當前點所在強連通份量的根節點修改成搜索到的這個節點,也就是根節點.. } } if(dfn[u] == low[u]) { //按照上面的定義咱們知道這是判斷是不是一個強連通份量的根節點 while(Stack[top] != u) { //按照上面所說的將該點在棧後的全部節點都彈出(在一個強連通份量裏..也就是咱們要求的了..能夠染色存儲起來備用或者縮成一個點(縮點算法)) printf("%d ",Stack[top]); vis[Stack[top--]] = 0; } printf("%d\n",Stack[top]); //彈出當前的這個根 vis[Stack[top--]] = 0; } }
首先來一張有向圖 \(G\).咱們一點一點來模擬整個算法.
遞歸
首先是一點一點的入棧..也就是上面DFS遍歷的時候的順序..叫作DFS序或者入棧序..即:get
Step1: 1號點入棧 dfn[1] = low[1] = ++index (1)
此時棧爲: 1
Step2: 2號點入棧 dfn[2] = low[2] = ++index (2)
此時棧爲: 1 2
Step3: 3號點入棧 dfn[3] = low[3] = ++index (3)
此時棧爲: 1 2 3
Step4: 6號點入棧 dfn[6] = low[6] = ++index (4)
此時棧爲: 1 2 3 6
左邊這張樹的圖就是當前操做完成後的DFS樹...
走到這裏以後咱們看到右邊圖中六號節點沒有了出邊...也就是上面說的第一步或者說是第一個判斷結束了...
咱們就要開始返回了,明顯 low[6] == dfn[6] 因此6就是一個強連通份量的根節點;
棧要一直彈出直到彈出6號節點 此時棧爲: 1 2 3
而後返回到三號..三號再無出邊..
也一直彈出直到將其彈出 存爲一個強連通份量的根 此時棧爲: 1 2
發現2號節點還有能夠繼續遍歷下去的邊..因而將五號節點壓入棧中即:
dfn[5] = dfn[5] = ++index(5) 此時棧爲: 1 2 5
再遍歷發現一個6號..已經遍歷過就不在管他了..
再遍歷發現一個1號節點..1號在棧中..因而進入第二個if語句,修改
low[5] = min(lowhttp://www.javashuo.com/tag/5,lowhttp://www.javashuo.com/tag/1) 因此 low[5] 也就是五號節點所在的強連通份量的根就是1
五號沒有出邊了..返回上一層..修改 low[2] = min(lowhttp://www.javashuo.com/tag/2,low5),low[2] = 1
二號沒有出邊了..返回上一層..修改 low[1] = min(lowhttp://www.javashuo.com/tag/1,low2),low[1] = 1 low[1]依然等於1
一號還有出邊..遍歷到四號 dfn[4] = low[4] = ++index(6) 此時棧爲 1 2 5 4
四號遍歷到五號..五號在棧中因此更新一下 low[4] = min(low4,low5),low[4] = 1;
再返回 low[1] = min(lowhttp://www.javashuo.com/tag/1,low4) ,low[1] = 1;
而後1號也沒有出邊了..這時棧一直彈出直到將1號彈出 棧空
最後的DFS樹是這樣的
按照咱們找到的根節點拆成一個個的強連通份量
這樣就完成了
咱們把以一號爲開始的連通圖都遍歷一遍了...爲了防止圖有多個(不連通) 咱們要在調用Tarjan的時候這樣寫
for(int i = 1; i <= n; ++i) if(!dfn[i]) Tarjan(i); //若是沒有時間戳,那就表明沒有遍歷到,今後點開始Tarjan
這樣就能夠把全部的圖都給遍歷一遍了...
來一道裸題。
輸入:
一個圖有向圖。
輸出:
它每一個強連通份量。
這個圖就是剛纔講的那個圖。如出一轍。
Input:
6 8
1 3
1 2
2 4
3 4
3 5
4 6
4 1
5 6
Output:
6
5
3 4 2 1
代碼:
#include <cstdio> #include <algorithm> using namespace std; struct node { int to,next; } edge[1001]; int cnt,Index,top; int dfn[1001],low[1001]; int stack[1001],head[1001],visit[1001]; void add(int x,int y) { edge[++cnt].next = head[x]; edge[cnt].to = y; head[x] = cnt; } void tarjan(int x) { //表明第幾個點在處理。遞歸的是點。 dfn[x] = low[x] = ++Index; // 新進點的初始化。 stack[++top] = x; //進棧 visit[x] = 1; //表示在棧裏 for(int i = head[x]; i; i = edge[i].next) { if(!dfn[edge[i].to]) { //若是沒訪問過 tarjan(edge[i].to); //往下進行延伸,開始遞歸 low[x] = min(low[x],low[edge[i].to]);//遞歸出來,比較誰是誰的兒子/父親,就是樹的對應關係,涉及到強連通份量子樹最小根的事情。 } else if(visit[edge[i].to]) { //若是訪問過,而且還在棧裏。 low[x] = min(low[x],dfn[edge[i].to]);//比較誰是誰的兒子/父親。就是連接對應關係 } } if(low[x] == dfn[x]) { //發現是整個強連通份量子樹裏的最小根。 do { printf("%d ",stack[top]); visit[stack[top]] = 0; top--; } while(x != stack[top+1]);//出棧,而且輸出。 printf("\n"); } return ; } int main() { int n,m; scanf("%d%d",&n,&m); int x,y; for(int i = 1; i <= m; i++) { scanf("%d%d",&x,&y); add(x,y); } for(int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i);//當這個點沒有訪問過,就今後點開始。防止圖沒走完 return 0; }