【强连通分量】tarjan算法及kosaraju算法+例题

阅读前请确保自己知道强连通分量是什么,本文不做赘述。

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 }

Korasaju算法

一、算法简介

  Korasaju算法比Tarjan时间复杂度要高,应用范围小,还有着爆栈超内存的风险,但这个算法比Tarjan好理解很多,虽然很玄学。当然和Tarjan一样,Korasaju也只能用于有向图中。

  Korasaju也是基于深度优先搜索的算法。这个算法牵扯到两个概念,发现时间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 上白泽慧音

时间限制: 1 s

空间限制: 128000 KB

题目等级 : 黄金 Gold

题目描述 Description

在幻想乡,上白泽慧音是以知识渊博闻名的老师。春雪异变导致人间之里的很多道路都被大雪堵塞,使有的学生不能顺利地到达慧音所在的村庄。因此慧音决定换一个能够聚集最多人数的村庄作为新的教学地点。人间之里由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。

输入描述 Input Description

第1行:两个正整数N,M

第2..M+1行:每行三个正整数a,b,t, t = 1表示存在从村庄a到b的单向道路,t = 2表示村庄a,b之间存在双向通行的道路。保证每条道路只出现一次。

输出描述 Output Description

第1行: 1个整数,表示最大的绝对连通区域包含的村庄个数。

第2行:若干个整数,依次输出最大的绝对连通区域所包含的村庄编号。

样例输入 Sample Input

5 5

1 2 1

1 3 2

2 4 2

5 1 2

3 5 1

样例输出 Sample Output

3

1 3 5

数据范围及提示 Data Size & Hint

对于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 

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 }
时间: 2024-10-10 22:06:44

【强连通分量】tarjan算法及kosaraju算法+例题的相关文章

图之强连通、强连通图、强连通分量 Tarjan算法

强连通分量 简介 在阅读下列内容之前,请务必了解图论基础部分. 强连通的定义是:有向图 G 强连通是指,G 中任意两个结点连通. 强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图. 这里想要介绍的是如何来求强连通分量. Tarjan 算法 Robert E. Tarjan (1948~) 美国人. Tarjan 发明了很多算法结构.光 Tarjan 算法就有很多,比如求各种联通分量的 Tarjan 算法,求 LCA(Lowest Comm

HDU 1269 强连通分量tarjan算法

迷宫城堡 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submission(s): 6655    Accepted Submission(s): 2973 Problem Description 为了训练小希的方向感,Gardon建立了一座大城堡,里面有N个房间(N<=10000)和M条通道(M<=100000),每个通道都是单向的,就是说若称某通道连通了A房

强连通分量(tarjan求强连通分量)

双DFS方法就是正dfs扫一遍,然后将边反向dfs扫一遍.<挑战程序设计>上有说明. 双dfs代码: 1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 #include <vector> 5 6 using namespace std; 7 const int MAXN = 1e4 + 5; 8 vector <int> G[MAXN]; //图的邻接表

ZOJ 3795 Grouping 强连通分量-tarjan

一开始我还天真的一遍DFS求出最长链以为就可以了 不过发现存在有向环,即强连通分量SCC,有向环里的每个点都是可比的,都要分别给个集合才行,最后应该把这些强连通分量缩成一个点,最后保证图里是 有向无环图才行,这个时候再找最长链,当然缩点之后的scc是有权值的,不能只看成1,缩点完了之后,用记忆化搜索DP就可以再On的复杂度内求出结果 所以现学了一下SCC-Tarjan,所谓Scc-tarjan,就是找到强连通分量并且缩点,特别好用,其原理就是利用dfs时间戳,每个点有自己的时间戳,同时再开一个记

强连通分量tarjan

#include<iostream>//强连通分量tarjan #include<cstdio> #include<algorithm> #include<queue> #include<cstring> using namespace std; #define maxn 400010 #define INF 1e18 int low[maxn],dfn[maxn],sta[maxn],d[maxn]/*value*/,dis[maxn]/*to

求图的强连通分量--tarjan算法

一:tarjan算法详解 ?思想: ? ?做一遍DFS,用dfn[i]表示编号为i的节点在DFS过程中的访问序号(也可以叫做开始时间)用low[i]表示i节点DFS过程中i的下方节点所能到达的开始时间最早的节点的开始时间.(也就是之后的深搜所能到达的最小开始时间)初始时dfn[i]=low[i] ? ?在DFS过程中会形成一搜索树.在搜索树上越先遍历到的节点,显然dfn的值就越小. ? ?DFS过程中,碰到哪个节点,就将哪个节点入栈.栈中节点只有在其所属的强连通分量已经全部求出时,才会出栈. ?

poj 2186 Popular Cows 【强连通分量Tarjan算法 + 树问题】

题目地址:http://poj.org/problem?id=2186 Popular Cows Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 27496   Accepted: 11059 Description Every cow's dream is to become the most popular cow in the herd. In a herd of N (1 <= N <= 10,000) cows

图论-强连通分量-Tarjan算法

有关概念: 如果图中两个结点可以相互通达,则称两个结点强连通. 如果有向图G的每两个结点都强连通,称G是一个强连通图. 有向图的极大强连通子图(没有被其他强连通子图包含),称为强连通分量.(这个定义在百科上和别的大神的博客中不太一样,暂且采用百科上的定义) Tarjan算法的功能就是求有向图中的强连通分量 思路: 定义DFNi存放访问到i结点的次序(时间戳),Lowi存放i结点及向i下方深搜到的结点中能追溯到的访问次序最小的结点的访问次序(即这些结点回溯上去能找到的最小的DFN值),找到未被访问

强连通分量--tarjan算法

今天学了一个强连通分量,用tarjan做.北京之前讲过,今天讲完和之前一样,没有什么进步.上课没听讲,只好回来搞,这里安利一个博客:链接 https://blog.csdn.net/qq_34374664/article/details/77488976 讲一下我自己的体会吧,其实就是维护一个栈,然后树上跑dfs,每个节点存两个值:dn和low,dn代表dfs的顺序(时间),low代表的是他可以连通的最小的节点. 模拟一下,然后就会发现,其实整个算法就是模拟了一下将每个点压入栈.然后遇到之前在栈