什么是强连通分量
强连通分量,是一个有向图的最大强连通子图(看起来好像没有什么解释效果…),好吧,强连通分量就是在一个有向图中,从任意一个点出发,最多可以走过的所有的点构成的一个点集,将其称之为强连通。强连通分量就是这些集合里面最大的那个。如下图:
2,3,5,就是强连通的,因为从二出发可以到达2,3,5的任意一个点。所以1,4,也是强连通的。我们不难发现,除了双向边以外,强连通都是环套环的形式。今天,我们的算法也会在这张图上展开。
算法详解
其实求强连通的tarjan算法的本质就是求环套环,我们将以邻接表为基础讲讲求强连通的tarjan算法。不懂邻接表的朋友看这里:邻接表存图法,那么这个算法就是要求环套环了嘛。
怎么求环套环呢?本算法中引入了时间戳这个概念,就是在进行dfs遍历(我相信看这个blog的朋友都会dfs把)的时候,遍历到的第一个点就打上1这个时间戳,第二个点就打上2这个时间戳。第n个点就打上n这个时间戳,我们把存每个点的时间戳的数组取名为dfn,则dfn[i]就是第i个点的时间戳。那么在dfs的时候,就可以免去记录此点是否走过的数组,而直接判断,凡是有时间戳的都是已经走过的点。
我们再使用一个数组color来染色,凡是在一个强连通分量里边的就把它染成一个相同的数字。比如2,3,5在一个强连通分量里面,那color[2],color[3],color[5]就把它赋值成一个数字(什么时候染色一会讲)。我们还需要一个数组low,每一个点都有自己的low,记录方法和dfn是一样的,每一个点的low初始是和dfn一样的,当我们dfs的时候,如果形成环路,那就必然会dfs到一个已经dfs过得点,而且这个点的时间戳还比自己小,那么就需要把那个已经dfs过的点的low拿来更新自己的low,然后一路递归地更新回去,这样,在这个环里dfs过的点就会有相同的low值。例如:上图中2 dfs 到3,3到5,至此,点2,3,5的low值都是自己的dfn,但是你会发现点5除了点2搜不到任何点了,那么就拿2的low更新自己的,然后递归回去,也一路更新回去,点3的用点5的更新…
然而这还是不能完成染色的,尽管在一个环里面的low都已经是一样的了。因此,我们创建一个栈。每搜到一个点,就把它加入栈中。什么时候退栈呢?比如我们从2开始搜索的,最后到了点5,然后2,3,5,都已经在栈中了,开始return了,于是点5退到了点2,然后会发现点2的low和dfn是一样的(因为大家其实都是拿点2来更新自己,所以2自己的没动),那就开始退栈。每退出一个元素,就把这个元素染色,直到把自己退掉。至此,染色完成,所有染成一样的点都在一个强连通分量里面。
实例模拟
我们就这个图进行一次模拟。
我们从点1开始深搜,如图,点1被打上时间戳(D)和low(L),并被加入栈中
打时间戳和low并入栈代码如下:
dfn[u]=low[u]=++cnt;
st[++top]=u;
其中u就是当前被深搜的点。
然后我们搜到了点二,如图:
点2也被打上时间戳和low,并且加入栈中,此时栈中有点1,2.
以此法,最后会这样:
此时栈中有:1 2 3 5
然后5搜到了2,但是2已经搜过了,而且2不在别的强连通分量里面,所以需要用2的low更新5的(对于5来说2就是下一个点,而邻接表结构体成员中恰恰会存u,v,也就是5到2,所以我们可以很方便的取到2的low值)。5的更新完毕后,就开始return了,return到3,也要用5的low更新3的。所以更新到2会是这样:
这里我给出深搜的代码和更新的代码(其实这两个本来就是一起写的):
for(int i=head[u];i;i=e[i].next){
if(dfn[e[i].v]==0){
tarjan(e[i].v);
low[u]=min(low[u],low[e[i].v]);
}
else if(color[e[i].v]==0){
low[u]=min(low[u],low[e[i].v]);
}
}
其中,用2更新5的时候,其实是执行else if语句里面的更新,因为2走过了,所以dfn[e[2].v]不可能等于0,因此第一个if他根本不会进去的。所以才需要第二的if,else if(color[e[i].v]==0)用于判断其是否已经属于别的强连通分量了,被染色当然就不会等于0了。属于别人的我们不要,我们要单身的~~不要二手——
好了,现在又回到了点2,按我刚刚描述的,要开始退栈染色了兄弟。代码如下:
if(low[u]==dfn[u]){
color[u]=++col;
while(st[top]!=u){
color[st[top]]=col;
top--;
}
top--;
}
这段代码紧跟在循环的下面(不在循环内部),这段代码只有在递归的时候才会触发,到了点2的时候(我们递归回到了点2),然后发现点2的low和时间戳都是一样的。所以就进入这个判断。一进入判断,先把当前的点更新一下,因为这个while是先判断后循环,所以到最后是不会染色自己的(一旦st[top]==u,就直接退出了,不再color[st[top]]=col了)。当然,你也可以尝试写do-while循环,这样就不需要事先给自己染色。col其实就是本次染色要染成的数字。while循环下面还有一个top----,目的是使自己——当前的点也可以退出栈,因为st[top]==u就退出了循环,也就没有top----了,原理和刚刚那个事先染色的差不多。
大家可能会有一个问题,递归回来的路上,都会先经过low[u]=min(low[u],low[e[i].v]);然后被上一个点更新,导致low和dfn不同,然后进入不了if(low[u]==dfn[u])这个判断,凭什么点2就可以呢?因为,我们一开始就是拿点2更新的,所以点2的确是走过了low[u]=min(low[u],low[e[i].v]);这个赋值,然而走过了之后自己还是自己啊,自己就是最小的。所以点2的low和dfn其实都还是原来的,而且还相等,所以…进入判断了。
点2的判断也结束后,就回到了点1,于是点1就搜到了点4,所以点4也被打上时间戳,计数变量cnt不会重置,所以点4的时间戳就是4,low也是4。如图:
然后点4又搜回了点1(这两个是双向的),但是点1已经是搜过的,所以执行else if那一句,把自己的low变成了点1的了,然后再return回去,直到深搜退出。最终会是酱紫:
下面我给出完整的代码和样例输入输出
样例输入 | 样例输出 |
---|---|
5 7 | 2 |
1 2 | 1 |
1 4 | 1 |
1 5 | 2 |
2 3 | 1 |
3 5 | |
4 1 | |
5 2 | |
1 |
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxm=1001;
int p,s,a,b;
int dfn[maxm],low[maxm],cnt;//时间戳、最小戳、计数
int st[maxm],top;
int color[maxm],col;//染色、强连通分量个数
struct edge{
int u,v,next;
}e[maxm];
int head[maxm],js;
void addedge(int u,int v){
e[++js].u=u;
e[js].v=v;
e[js].next=head[u];
head[u]=js;
return;
}
void tarjan(int u){
dfn[u]=low[u]=++cnt;
st[++top]=u;
for(int i=head[u];i;i=e[i].next){
if(dfn[e[i].v]==0){
tarjan(e[i].v);
low[u]=min(low[u],low[e[i].v]);
}
else if(color[e[i].v]==0){
low[u]=min(low[u],low[e[i].v]);
}
}
if(low[u]==dfn[u]){
color[u]=++col;
while(st[top]!=u){
color[st[top]]=col;
top--;
}
top--;
}
return;
}
int main(){
cin>>p>>s;
for(int i=1;i<=s;i++){
cin>>a>>b;
addedge(a,b);
}
int start;
cin>>start;
tarjan(start);
for(int i=1;i<=p;i++) cout<<color[i]<<endl;
return 0;
}
手累,大家多模拟模拟就一定可以学会哦…