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

时间:2022-10-13 17:44:15

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

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等等。

二、算法模板

 void tarjan(int pos){
vis[stack[++index]=pos]=;//入栈并标记
LOW[pos]=DFN[pos]=++dfs_num;
for(int i=pre[pos];i;i=E[i].next){
if(!DFN[E[i].to]){
tarjan(E[i].to);
LOW[pos]=min(LOW[pos],LOW[E[i].to]);
}
else if(vis[E[i].to]) LOW[pos]=min(LOW[pos],DFN[E[i].to]);
}
if(LOW[pos]==DFN[pos]){
vis[pos]=;
size[dye[pos]=++CN]++;//染色及记录强连通分量大小
while(pos!=stack[index]){
vis[stack[index]]=;
size[CN]++;//记录大小
dye[stack[index--]]=CN;//弹栈并染色
}
index--;
}
}

Kosaraju算法

一、算法简介

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

  Kosaraju也是基于深度优先搜索的算法。这个算法牵扯到两个概念,发现时间st,完成时间et。发现时间是指一个节点第一次被遍历到时的次序号,完成时间是指某一结点最后一次被遍历到的次序号

  在加边时把有向图正向建造完毕后再反向加边建一张逆图

  先对正图进行一遍dfs,遇到没访问过的点就让其发现时间等于目前的dfs次序号。在回溯时若发现某一结点的子树全部被遍历完,就让其完成时间等于目前dfs次序号。正图遍历完后将节点按完成时间入栈,保证栈顶是完成时间最大的节点,栈底是完成时间最小的节点

  (玄学内容开始)然后从栈顶开始向下每一个没有被反向遍历过的节点为起点对逆图进行一遍dfs,将访问到的点记录下来(或染色)并弹栈,每一遍反向dfs遍历到的点就构成一个强连通分量。虽然不知道为什么但他就成强连通分量了...

二、算法模板

 void positive_dfs(int pos){
DFN++;
vis[pos]=;
for(int i=pre[][pos];i;i=E[][i].next)
if(!vis[E[][i].to])
positive_dfs(E[][i].to);
stack[N*+-(++DFN)]=pos;
}
void negative_dfs(int pos){
dye[pos]=CN;
vis[pos]=;
size[dye[pos]]++;
for(int i=pre[][pos];i;i=E[][i].next)
if(vis[E[][i].to])
negative_dfs(E[][i].to);
}
int main(){ ...... for(int i=;i<=N;i++)
if(!vis[i])
positive_dfs(i);
for(int i=;i<=N*;i++)
if(stack[i]&&vis[stack[i]]){
CN++;
negative_dfs(stack[i]);
} ...... }

三、例题/裸题

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:

 #include <stdio.h>
#include <algorithm>
using namespace std;
int n,m,cnt,index,dfs_num,CN,maxn=-;
int pre[],vis[],DFN[],LOW[],stack[],dye[],size[];
struct pack{int to,next;} E[];
void add_edge(int x,int y){
E[++cnt].to=y;
E[cnt].next=pre[x];
pre[x]=cnt;
}
void input(){
scanf("%d%d",&n,&m);
for(int i=;i<=m;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add_edge(a,b);
if(c==) add_edge(b,a);
}
}
void tarjan(int pos){
vis[stack[++index]=pos]=;
LOW[pos]=DFN[pos]=++dfs_num;
for(int i=pre[pos];i;i=E[i].next){
if(!DFN[E[i].to]){
tarjan(E[i].to);
LOW[pos]=min(LOW[pos],LOW[E[i].to]);
}
else if(vis[E[i].to]) LOW[pos]=min(LOW[pos],DFN[E[i].to]);
}
if(LOW[pos]==DFN[pos]){
vis[pos]=;
size[dye[pos]=++CN]++;
while(pos!=stack[index]){
vis[stack[index]]=;
size[CN]++;
dye[stack[index--]]=CN;
}
index--;
}
}
void output(){
int lenn=;
int tar;
for(int i=;i<=n;i++)
if(size[dye[i]]>maxn) maxn=size[dye[i]],tar=i;
printf("%d\n",maxn);
for(int i=;i<=n;i++)
if(dye[i]==dye[tar]) printf("%d ",i);
}
int main(){
input();
for(int i=;i<=n;i++)
if(!dye[i]) tarjan(i);
output();
return ;
}

Korasaju:

 #include <stdio.h>
#include <algorithm>
#include <cstring>
using namespace std;
int N,M,DFN,CN,tot;
int cnt[],vis[],stack[],dye[],size[];
int pre[][];
struct pack{
int to;
int next;
}E[][];
void add_edge(int x,int y,int f){
E[f][++cnt[f]].to=y;
E[f][cnt[f]].next=pre[f][x];
pre[f][x]=cnt[f];
}
void positive_dfs(int pos){
DFN++;
vis[pos]=;
for(int i=pre[][pos];i;i=E[][i].next)
if(!vis[E[][i].to])
positive_dfs(E[][i].to);
stack[N*+-(++DFN)]=pos;
}
void negative_dfs(int pos){
dye[pos]=CN;
vis[pos]=;
size[dye[pos]]++;
for(int i=pre[][pos];i;i=E[][i].next)
if(vis[E[][i].to])
negative_dfs(E[][i].to);
}
int main(){
scanf("%d%d",&N,&M);
for(int i=;i<=M;i++){
int a,b,t;
scanf("%d%d%d",&a,&b,&t);
add_edge(a,b,);
add_edge(b,a,);
if(t==){
add_edge(b,a,);
add_edge(a,b,);
}
}
for(int i=;i<=N;i++)
if(!vis[i])
positive_dfs(i);
for(int i=;i<=N*;i++)
if(stack[i]&&vis[stack[i]]){
CN++;
negative_dfs(stack[i]);
}
int maxn=-,tar;
for(int i=;i<=N;i++)
if(size[dye[i]]>maxn) maxn=size[dye[i]],tar=i;
printf("%d\n",maxn);
for(int i=;i<=N;i++)
if(dye[i]==dye[tar]) printf("%d ",i);
return ;
}

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算法及kosaraju算法+例题

由于这道题我只用了Tarjan写,所以只付上Tarjan AC代码。由于我是用染色处理的,所以Korasaju应该也能写。

 #include <stdio.h>
#include <algorithm>
using namespace std;
int n,m,cnt,re_cnt,top,dfs_num,CN;
int pre[],re_pre[],tow[],DFN[],LOW[],dye[],size[],vis[];
struct pack{int to,next;} E[],re_E[];
void add_edge(int x,int y){
E[++cnt].to=y;
E[cnt].next=pre[x];
pre[x]=cnt;
}
void tarjan(int pos){
vis[tow[++top]=pos]=;
DFN[pos]=LOW[pos]=++dfs_num;
for(int i=pre[pos];i;i=E[i].next){
if(!DFN[E[i].to]){
tarjan(E[i].to);
LOW[pos]=min(LOW[pos],LOW[E[i].to]);
}
else if(vis[E[i].to])
LOW[pos]=min(LOW[pos],DFN[E[i].to]);
}
if(LOW[pos]==DFN[pos]){
vis[pos]=;
size[dye[pos]=++CN]++;
while(pos!=tow[top]){
vis[tow[top]]=;
size[dye[tow[top--]]=CN]++;
}
top--;
}
}
void rebuild(){
for(int i=;i<=n;++i)
for(int j=pre[i];j;j=E[j].next)
if(dye[i]!=dye[E[j].to]){
re_E[++re_cnt].next=re_pre[dye[i]];//这里写复杂了,其实光统计出度就够了,不用彻底重建图
re_E[re_cnt].to=dye[E[j].to];
re_pre[dye[i]]=re_cnt;
}
}
int cal(){
int ret=;
for(int i=;i<=CN;++i)
if(!re_pre[i]){
if(ret) return ;
else ret=size[i];
}
return ret;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=;i<=m;++i){
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
}
for(int i=;i<=n;++i)
if(!dye[i]) tarjan(i);
rebuild();
printf("%d",cal());
return ;
}