閱讀前請確保自己知道強連通分量是什么,本文不做贅述。
Tarjan算法
一、算法簡介
Tarjan算法是一種由Robert Tarjan提出的求有向圖強連通分量的時間復雜度為O(n)的算法。
首先我們要知道兩個概念:時間戳(DFN),節點能追溯到的最早的棧中節點的時間戳(LOW)。顧名思義,DFN就是在搜索中某一節點被遍歷到的次序號(dfs_num),LOW就是某一節點在棧中能追溯到的最早的父親節點的搜索次序號。
Tarjan算法是基於深度優先搜索的算法。在搜索過程中把沒有Tarjan過的點入棧,並將該節點的DFN[i]=LOW[i]=++dfs_num(也就是設成他自己),然后以這個節點為樹根再進行搜索。當一顆子樹搜索完畢時回溯,並在回溯時比較當前節點和目標節點的LOW值,將較小的LOW值賦給當前結點的LOW,這樣可以保證每個節點在以其為樹根的子樹的所有節點中LOW值是最小的。如果回溯時發現當前節點DFN[i]==LOW[i],就將棧中當前結點以上的節點全部彈棧,這些點就組成了一個強連通分量。還要注意一點是,當目標節點進行過Tarjan但還在棧中,就拿當前節點LOW值與目標節點DFN值比較,把更小的賦給當前結點的LOW。
所以總的來說我們在搜索過程中會遇到以下三種節點:從沒訪問過的節點(固然不在棧中),訪問過但不在棧中的節點,訪問過但在棧中的節點。對於三種點我們要分開討論。
①對於從沒訪問過的節點:加入棧中讓其DFN[i]=LOW[i]=++dfs_num,讓vis[i]=1表示該點入棧了。這類點的標志是!DFN[i]&&!vis[i]。
②對於訪問過但不在棧中的節點(!vis[i])直接回溯即可,因為既然該節點訪問過了又不在棧中,就必定屬於另一個強連通分量。這類點的標志是DFN[i]&&!vis[i]。
②對於訪問過且在棧中的節點,比較當前節點LOW值和目標節點DFN值,將較小的賦給當前結點LOW值然后回溯。這類點的標志是DFN[i]&&vis[i]。
在彈棧過程中可以將不同強連通分量染色,方便后續的其他處理(例如縮點,記錄不同強連通分量大小等)。
Tarjan除了用來求強連通分量,還可以用來縮點、求點雙連通分量、求LCA等等。
二、算法模板
1 void tarjan(int pos){ 2 vis[stack[++index]=pos]=1;//入棧並標記 3 LOW[pos]=DFN[pos]=++dfs_num; 4 for(int i=pre[pos];i;i=E[i].next){ 5 if(!DFN[E[i].to]){ 6 tarjan(E[i].to); 7 LOW[pos]=min(LOW[pos],LOW[E[i].to]); 8 } 9 else if(vis[E[i].to]) LOW[pos]=min(LOW[pos],DFN[E[i].to]); 10 } 11 if(LOW[pos]==DFN[pos]){ 12 vis[pos]=0; 13 size[dye[pos]=++CN]++;//染色及記錄強連通分量大小 14 while(pos!=stack[index]){ 15 vis[stack[index]]=0; 16 size[CN]++;//記錄大小 17 dye[stack[index--]]=CN;//彈棧並染色 18 } 19 index--; 20 } 21 }
Kosaraju算法
一、算法簡介
Kosaraju算法比Tarjan時間復雜度要高,應用范圍小,還有着爆棧超內存的風險,但這個算法比Tarjan好理解很多,雖然很玄學。當然和Tarjan一樣,Kosaraju也只能用於有向圖中。
Kosaraju也是基於深度優先搜索的算法。這個算法牽扯到兩個概念,發現時間st,完成時間et。發現時間是指一個節點第一次被遍歷到時的次序號,完成時間是指某一結點最后一次被遍歷到的次序號。
在加邊時把有向圖正向建造完畢后再反向加邊建一張逆圖。
先對正圖進行一遍dfs,遇到沒訪問過的點就讓其發現時間等於目前的dfs次序號。在回溯時若發現某一結點的子樹全部被遍歷完,就讓其完成時間等於目前dfs次序號。正圖遍歷完后將節點按完成時間入棧,保證棧頂是完成時間最大的節點,棧底是完成時間最小的節點。
(玄學內容開始)然后從棧頂開始向下每一個沒有被反向遍歷過的節點為起點對逆圖進行一遍dfs,將訪問到的點記錄下來(或染色)並彈棧,每一遍反向dfs遍歷到的點就構成一個強連通分量。雖然不知道為什么但他就成強連通分量了...
二、算法模板
1 void positive_dfs(int pos){ 2 DFN++; 3 vis[pos]=1; 4 for(int i=pre[1][pos];i;i=E[1][i].next) 5 if(!vis[E[1][i].to]) 6 positive_dfs(E[1][i].to); 7 stack[N*2+1-(++DFN)]=pos; 8 } 9 void negative_dfs(int pos){ 10 dye[pos]=CN; 11 vis[pos]=0; 12 size[dye[pos]]++; 13 for(int i=pre[2][pos];i;i=E[2][i].next) 14 if(vis[E[2][i].to]) 15 negative_dfs(E[2][i].to); 16 } 17 int main(){ 18 19 ...... 20 21 for(int i=1;i<=N;i++) 22 if(!vis[i]) 23 positive_dfs(i); 24 for(int i=1;i<=N*2;i++) 25 if(stack[i]&&vis[stack[i]]){ 26 CN++; 27 negative_dfs(stack[i]); 28 } 29 30 ...... 31 32 }
三、例題/裸題
codevs1332 上白澤慧音
在幻想鄉,上白澤慧音是以知識淵博聞名的老師。春雪異變導致人間之里的很多道路都被大雪堵塞,使有的學生不能順利地到達慧音所在的村庄。因此慧音決定換一個能夠聚集最多人數的村庄作為新的教學地點。人間之里由N個村庄(編號為1..N)和M條道路組成,道路分為兩種一種為單向通行的,一種為雙向通行的,分別用1和2來標記。如果存在由村庄A到達村庄B的通路,那么我們認為可以從村庄A到達村庄B,記為(A,B)。當(A,B)和(B,A)同時滿足時,我們認為A,B是絕對連通的,記為<A,B>。絕對連通區域是指一個村庄的集合,在這個集合中任意兩個村庄X,Y都滿足<X,Y>。現在你的任務是,找出最大的絕對連通區域,並將這個絕對連通區域的村庄按編號依次輸出。若存在兩個最大的,輸出字典序最小的,比如當存在1,3,4和2,5,6這兩個最大連通區域時,輸出的是1,3,4。
第1行:兩個正整數N,M
第2..M+1行:每行三個正整數a,b,t, t = 1表示存在從村庄a到b的單向道路,t = 2表示村庄a,b之間存在雙向通行的道路。保證每條道路只出現一次。
第1行: 1個整數,表示最大的絕對連通區域包含的村庄個數。
第2行:若干個整數,依次輸出最大的絕對連通區域所包含的村庄編號。
5 5
1 2 1
1 3 2
2 4 2
5 1 2
3 5 1
3
1 3 5
對於60%的數據:N <= 200且M <= 10,000
對於100%的數據:N <= 5,000且M <= 50,000
題解:一道強連通分量裸題,不做贅述。下面分別是Tarjan AC代碼和Korasaju AC代碼。
Tarjan:
1 #include <stdio.h> 2 #include <algorithm> 3 using namespace std; 4 int n,m,cnt,index,dfs_num,CN,maxn=-1; 5 int pre[50010],vis[50010],DFN[50010],LOW[50010],stack[50010],dye[50010],size[50010]; 6 struct pack{int to,next;} E[50010]; 7 void add_edge(int x,int y){ 8 E[++cnt].to=y; 9 E[cnt].next=pre[x]; 10 pre[x]=cnt; 11 } 12 void input(){ 13 scanf("%d%d",&n,&m); 14 for(int i=1;i<=m;i++){ 15 int a,b,c; 16 scanf("%d%d%d",&a,&b,&c); 17 add_edge(a,b); 18 if(c==2) add_edge(b,a); 19 } 20 } 21 void tarjan(int pos){ 22 vis[stack[++index]=pos]=1; 23 LOW[pos]=DFN[pos]=++dfs_num; 24 for(int i=pre[pos];i;i=E[i].next){ 25 if(!DFN[E[i].to]){ 26 tarjan(E[i].to); 27 LOW[pos]=min(LOW[pos],LOW[E[i].to]); 28 } 29 else if(vis[E[i].to]) LOW[pos]=min(LOW[pos],DFN[E[i].to]); 30 } 31 if(LOW[pos]==DFN[pos]){ 32 vis[pos]=0; 33 size[dye[pos]=++CN]++; 34 while(pos!=stack[index]){ 35 vis[stack[index]]=0; 36 size[CN]++; 37 dye[stack[index--]]=CN; 38 } 39 index--; 40 } 41 } 42 void output(){ 43 int lenn=0; 44 int tar; 45 for(int i=1;i<=n;i++) 46 if(size[dye[i]]>maxn) maxn=size[dye[i]],tar=i; 47 printf("%d\n",maxn); 48 for(int i=1;i<=n;i++) 49 if(dye[i]==dye[tar]) printf("%d ",i); 50 } 51 int main(){ 52 input(); 53 for(int i=1;i<=n;i++) 54 if(!dye[i]) tarjan(i); 55 output(); 56 return 0; 57 }
Korasaju:
1 #include <stdio.h> 2 #include <algorithm> 3 #include <cstring> 4 using namespace std; 5 int N,M,DFN,CN,tot; 6 int cnt[3],vis[50010],stack[70010],dye[50010],size[50010]; 7 int pre[3][50010]; 8 struct pack{ 9 int to; 10 int next; 11 }E[3][50010]; 12 void add_edge(int x,int y,int f){ 13 E[f][++cnt[f]].to=y; 14 E[f][cnt[f]].next=pre[f][x]; 15 pre[f][x]=cnt[f]; 16 } 17 void positive_dfs(int pos){ 18 DFN++; 19 vis[pos]=1; 20 for(int i=pre[1][pos];i;i=E[1][i].next) 21 if(!vis[E[1][i].to]) 22 positive_dfs(E[1][i].to); 23 stack[N*2+1-(++DFN)]=pos; 24 } 25 void negative_dfs(int pos){ 26 dye[pos]=CN; 27 vis[pos]=0; 28 size[dye[pos]]++; 29 for(int i=pre[2][pos];i;i=E[2][i].next) 30 if(vis[E[2][i].to]) 31 negative_dfs(E[2][i].to); 32 } 33 int main(){ 34 scanf("%d%d",&N,&M); 35 for(int i=1;i<=M;i++){ 36 int a,b,t; 37 scanf("%d%d%d",&a,&b,&t); 38 add_edge(a,b,1); 39 add_edge(b,a,2); 40 if(t==2){ 41 add_edge(b,a,1); 42 add_edge(a,b,2); 43 } 44 } 45 for(int i=1;i<=N;i++) 46 if(!vis[i]) 47 positive_dfs(i); 48 for(int i=1;i<=N*2;i++) 49 if(stack[i]&&vis[stack[i]]){ 50 CN++; 51 negative_dfs(stack[i]); 52 } 53 int maxn=-1,tar; 54 for(int i=1;i<=N;i++) 55 if(size[dye[i]]>maxn) maxn=size[dye[i]],tar=i; 56 printf("%d\n",maxn); 57 for(int i=1;i<=N;i++) 58 if(dye[i]==dye[tar]) printf("%d ",i); 59 return 0; 60 }
bzoj1051 受歡迎的牛
Time Limit: 10 Sec Memory Limit: 162 MB
Submit: 3673 Solved: 1940
Description
每一頭牛的願望就是變成一頭最受歡迎的牛。現在有N頭牛,給你M對整數(A,B),表示牛A認為牛B受歡迎。 這種關系是具有傳遞性的,如果A認為B受歡迎,B認為C受歡迎,那么牛A也認為牛C受歡迎。你的任務是求出有多少頭牛被所有的牛認為是受歡迎的。
Input
第一行兩個數N,M。 接下來M行,每行兩個數A,B,意思是A認為B是受歡迎的(給出的信息有可能重復,即有可能出現多個A,B)
Output
一個數,即有多少頭牛被所有的牛認為是受歡迎的。
Sample Input
3 3
1 2
2 1
2 3
Sample Output
1
HINT
100%的數據N<=10000,M<=50000
題解:這道題主要思路是求出強連通分量后將每個強連通分量合並縮成一個節點,然后求出出度為零的節點即可。注意,縮點后只能有一個出度為零的節點,如果有多個答案為0,若沒有出度為0的點答案也為0。

由於這道題我只用了Tarjan寫,所以只付上Tarjan AC代碼。由於我是用染色處理的,所以Korasaju應該也能寫。
1 #include <stdio.h> 2 #include <algorithm> 3 using namespace std; 4 int n,m,cnt,re_cnt,top,dfs_num,CN; 5 int pre[50010],re_pre[50010],tow[50010],DFN[50010],LOW[50010],dye[50010],size[50010],vis[50010]; 6 struct pack{int to,next;} E[50010],re_E[50010]; 7 void add_edge(int x,int y){ 8 E[++cnt].to=y; 9 E[cnt].next=pre[x]; 10 pre[x]=cnt; 11 } 12 void tarjan(int pos){ 13 vis[tow[++top]=pos]=1; 14 DFN[pos]=LOW[pos]=++dfs_num; 15 for(int i=pre[pos];i;i=E[i].next){ 16 if(!DFN[E[i].to]){ 17 tarjan(E[i].to); 18 LOW[pos]=min(LOW[pos],LOW[E[i].to]); 19 } 20 else if(vis[E[i].to]) 21 LOW[pos]=min(LOW[pos],DFN[E[i].to]); 22 } 23 if(LOW[pos]==DFN[pos]){ 24 vis[pos]=0; 25 size[dye[pos]=++CN]++; 26 while(pos!=tow[top]){ 27 vis[tow[top]]=0; 28 size[dye[tow[top--]]=CN]++; 29 } 30 top--; 31 } 32 } 33 void rebuild(){ 34 for(int i=1;i<=n;++i) 35 for(int j=pre[i];j;j=E[j].next) 36 if(dye[i]!=dye[E[j].to]){ 37 re_E[++re_cnt].next=re_pre[dye[i]];//這里寫復雜了,其實光統計出度就夠了,不用徹底重建圖 38 re_E[re_cnt].to=dye[E[j].to]; 39 re_pre[dye[i]]=re_cnt; 40 } 41 } 42 int cal(){ 43 int ret=0; 44 for(int i=1;i<=CN;++i) 45 if(!re_pre[i]){ 46 if(ret) return 0; 47 else ret=size[i]; 48 } 49 return ret; 50 } 51 int main(){ 52 scanf("%d%d",&n,&m); 53 for(int i=1;i<=m;++i){ 54 int a,b; 55 scanf("%d%d",&a,&b); 56 add_edge(a,b); 57 } 58 for(int i=1;i<=n;++i) 59 if(!dye[i]) tarjan(i); 60 rebuild(); 61 printf("%d",cal()); 62 return 0; 63 }
