若以节点序号顺序进行DFS,则形成下面一棵DFS树(粗边是树枝,其他类型的边在旁边有说明)
横叉边在图中是只能向左的,通过DFS的过程可以知道这一点。
这对我们设计算法很有帮助。
注意,边的分类与点的分类完全不同。
点的分类随着时间变化,而边的分类是不变的。
所以我们需要用一个全局变量保存边的分类。
在后面的程序中,我们用kl[i,j]表示i到j的边的分类,树枝、反向边、正向边、横叉边分别用1/2/3/4表示。
强连通分量一个有向图中,如果节点i能够通过一些边到达节点j,就简写成i能到达j。
如果对于任意两个节点i,j均有i能到达j或j能到达i,则说此图是连通的。
如果对于任意两个节点i,j均有i能到达j且j能到达i,则说此图是强连通的。
对于一个无向图,说强联通没有意义,因为此时强连通就是连通。
而对于一个有向图,它不一定是强连通的,但可以分为几个极大的强连通子图(“极大”的意思是再加入任何一个顶点就不满足强连通了)。
这些子图叫做这个有向图的强连通分量。
在上图中,强连通分量是A{1},B{2,4},C{3,5,6,7}。
在一个强连通分量中的节点由于有着相似的拓扑性质,所以我们可以将其紧缩为一个节点(这让我想到了什么?
化学里的“族”),于是就大大减小了图的规模。
比如上图紧缩为A,B,C三个节点后,整个图就成为了B←A→C的简单形式。
所以强连通分量是一个很有意义的东西。
然而如果根据定义,对每个节点进行一次O(n2)的DFS以求出它所能到达的顶点的话,整个算法的时间复杂度就是迟钝的O(n3)。
下面将介绍两种用O(n2)求强连通分量的算法:
Kosaraju算法和Tarjan算法。
Kosaraju算法Kosaraju算法基于以下思想:
强连通分量一定是某种DFS形成的DFS树森林。
Kosaraju算法给出了更具体的方式:
①任意进行一次DFS,记录下每个节点的结束时间戳f[i]。
②按f[i]的大小对节点进行排序(从小到大)。
③以②的排序结果逆序对原图的逆向图进行一次DFS,所得的DFS树森林即为强连通分量。
这次DFS可以用Floodfill进行,把每个强连通分量标上不同序号。
(这就是OI界传说中的“求强连两遍DFS的算法”)比如上图,我们可以得到:
d[1]=1 f[1]=14d[2]=2 f[2]=5d[3]=6 f[3]=13d[4]=3 f[4]=4d[5]=7 f[5]=8d[6]=9 f[6]=12d[7]=10f[7]=11根据f[i]排序得:
④②⑤⑦⑥③①(发现了什么?
就是后序遍历),再按照逆序对原图的逆向图进行一次DFS即可程序:
vara:
array[1..1000,1..1000]oflongint; b,flag:
array[1..1000]oflongint; n,i,j,m,k:
longint;
proceduredfs(x:
longint); varj:
longint; begin flag[x]:
=1; forj:
=1tondo if(flag[j]=0)and(a[x,j]>0)thendfs(j); inc(m); b[m]:
=x; end;
procedurefill(x,k:
longint); varj:
longint; begin flag[x]:
=k; forj:
=ndownto1do if(flag[b[j]]=0)and(a[b[j],x]>0)thenfill(b[j],k); end;
begin
readln(n); fori:
=1tondo begin forj:
=1tondoread(a[i,j]); readln; end; m:
=0; fillchar(flag,sizeof(flag),0); fori:
=1tondo ifflag[i]=0thendfs(i);
fillchar(flag,sizeof(flag),0); k:
=0; fori:
=ndownto1do ifflag[b[i]]=0then begin inc(k); fill(b[i],k); end;
fori:
=1tokdo begin forj:
=1tondo ifflag[j]=ithenwrite(j,''); writeln; end;
end.
kosaraju算法小结
kosaraju算法是用于求有向图的强连通分量的算法之一
步骤概要:
1.DFS有向图G,并以后根序记录节点
2.把存在于记录集中且最后访问节点作为起点,DFS反图GT,并以先根序把节点从记录中剔除;
3.若此次不能DFS反图GT所有节点,则重复步骤2,直到所有节点都被剔除出记录;每次剔除掉的节点集即为原有向图G的一个强连通分量
简要证明:
1.第一次DFS有向图G时,最后记录下的节点必为最后一棵生成树的根节点。
证明:
假设最后记录下节点不是树根,则必存在一节点为树根,且树根节点必为此节点祖先;而由后根序访问可知祖先节点比此节点更晚访问,矛盾;原命题成立
2.第一次DFS的生成森林中,取两节点A、B,满足:
B比A更晚记录下,且B不是A的祖先(即在第一次DFS中,A、B处于不同的生成树中);则在第二次DFS的生成森林中,B不是A的祖先,且A也不是B的祖先(即在第二次DFS中,A、B处于不同的生成树中)。
证明:
假设在第二次DFS的生成森林中,B是A的祖先,则反图GT中存在B到A路径,即第一次DFS生成森林中,A是B的祖先,则A必比B更晚记录下,矛盾;假设在第二次DFS的生成森林中,A是B的祖先,则反图GT中存在A到B路径,即第一次DFS生成森林中,B是A的祖先,矛盾;原命题成立
3.按上述步骤求出的必为强连通分量证明:
首先,证明2保证了第二次DFS中的每一棵树都是第一次DFS中的某棵树或某棵树的子树。
其次,对于第二次DFS中的每棵树,第一次DFS保证了从根到其子孙的连通性,第二次DFS保证了根到子孙的反向连通性(即子孙到根的连通性);由此,此树中的每个节点都通过其根相互连通。
Tarjan算法Tarjan算法只要一遍DFS,效率高于Kosaraju。
它在技术上有了很大改进。
它基于的思想是:
强连通分量是DFS树中的子树(无论你如何进行DFS)。
Tarjan算法的过程是:
①在DFS树中,设low[x]是x或x的后代能够达到的最高的祖先。
初始化时low[x]设为x。
②进行DFS,在结束节点x时计算low[x]。
计算的方法是:
找出x能够到达的所有节点i,应保证low[i]是x的祖先,即low[i]是灰点。
low[x]=highest(low[i]),即d[low[i]]最小。
③若low[x]=x,则以x为根的子树就是一个强连通分量,输出并将其从树中删去。
比如上图(又是上图。
。
没办法,图片空间有限制),我们依次得出:
low[4]=low[2]=2low[2]=low[4]=2 //{4,2}为强连通分量low[5]=low[3]=3low[7]=low[5]=3low[6]=low[7]=3low[3]=low[6]=3 //{5,7,6,3}为强连通分量low[1]=1 //{1}为强连通分量感谢ChenGang同学的提醒,我原来的程序中的输出有些问题。
现在采用堆栈的方法,dfs到某一节点时将其入栈,当low[x]=x时不断出栈,直到将x出栈为止,因为x是这个强连通分量的根。
程序:
vara,kl:
array[1..1000,1..1000]oflongint; d,f,low,stack:
array[1..1000]oflongint; i,j,n,time,h:
longint;
proceduredfs(x:
longint); vari:
longint; begin inc(time); d[x]:
=time; inc(h); stack[h]:
=x; fori:
=1tondo ifa[x,i]>0then begin ifd[i]=0then begin kl[x,i]:
=1; dfs(i); end; if(d[i]>0)and(f[i]=0)thenkl[x,i]:
=2; iff[i]>0then begin ifd[x]>d[i]thenkl[x,i]:
=3elsekl[x,i]:
=4; end; end; inc(time); f[x]:
=time; fori:
=1tondo ifa[x,i]>0then if(f[low[i]]=0)and(d[low[i]]0)then low[x]:
=low[i]; iflow[x]=xthen begin whilestack[h]<>xdo begin write(stack[h],''); dec(h); end; writeln(stack[h]); dec(h); end; end;
begin
readln(n); fori:
=1tondo begin forj:
=1tondoread(a[i,j]); readln; end;
fillchar(d,sizeof(d),0); fillchar(d,sizeof(d),0); fori:
=1tondolow[i]:
=i; time:
=0; fillchar(stack,sizeof(stack),0); h:
=0; fori:
=1tondo ifd[i]=0thendfs(i);
end.
有人会问,这里边的分类用不到啊?
我会郑重地回答:
就是没用。
实际上,原因在于:
BYVoid神牛的Blog里也讲到了Tarjan算法(图文并茂啊。
。
我就是看这个学的),而里面涉及了边的分类却没有给予解释,对于初学者(又是我?
)不利。
所以我就写了这些。
有人会问,这里边的分类用不到啊?
我会郑重地回答:
就是没用。
实际上,原因在于:
BYVoid神牛的Blog里也讲到了Tarjan算法(图文并茂啊。
。
我就是看这个学的),而里面涉及了边的分类却没有给予解释,对于初学者(又是我?
)不利。
所以我就写了这些。
以下无正文
仅供个人用于学习、研究;不得用于商业用途。
толькодлялюдей,которыеиспользуютсядляобучения,исследованийинедолжныиспользоватьсявкоммерческихцелях.
Forpersonaluseonlyinstudyandresearch;notforcommercialuse.
NurfürdenpersönlichenfürStudien,Forschung,zukommerziellenZweckenverwendetwerden.
Pourl'étudeetlarechercheuniquementàdesfinspersonnelles;pasàdesfinscommerciales.
Forpersonaluseonlyinstudyandresearch;notforcommercialuse