强连通分量
一个有向图中,如果节点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]=14
d[2]=2 f[2]=5
d[3]=6 f[3]=13
d[4]=3 f[4]=4
d[5]=7 f[5]=8
d[6]=9 f[6]=12
d[7]=10 f[7]=11
根据f[i]排序得:④②⑤⑦⑥③①(发现了什么?就是后序遍历),再按照这个顺序进行DFS即可。
程序:
代码
1 var a:array[1..1000,1..1000]of longint;
2 b,flag:array[1..1000]of longint;
3 n,i,j,m,k:longint;
4
5 procedure dfs(x:longint);
6 var j:longint;
7 begin
8 flag[x]:=1;
9 for j:=1 to n do
10 if (flag[j]=0)and(a[x,j]>0) thendfs(j);
11 inc(m);
12 b[m]:=x;
13 end;
14
15 procedurefill(x,k:longint);
16 var j:longint;
17 begin
18 flag[x]:=k;
19 for j:=n downto 1 do
20 if (flag[b[j]]=0)and(a[b[j],x]>0)then fill(b[j],k);
21 end;
22
23 begin
24
25 readln(n);
26 for i:=1 to n do
27 begin
28 for j:=1 to n do read(a[i,j]);
29 readln;
30 end;
31 m:=0;
32 fillchar(flag,sizeof(flag),0);
33 for i:=1 to n do
34 if flag[i]=0 then dfs(i);
35
36 fillchar(flag,sizeof(flag),0);
37 k:=0;
38 for i:=n downto 1 do
39 if flag[b[i]]=0 then
40 begin
41 inc(k);
42 fill(b[i],k);
43 end;
44
45 for i:=1 to k do
46 begin
47 for j:=1 to n do
48 if flag[j]=i then write(j,' ');
49 writeln;
50 end;
51
52 end.
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]=2
low[2]=low[4]=2 //{4,2}为强连通分量
low[5]=low[3]=3
low[7]=low[5]=3
low[6]=low[7]=3
low[3]=low[6]=3 //{5,7,6,3}为强连通分量
low[1]=1 //{1}为强连通分量
感谢ChenGang同学的提醒,我原来的程序中的输出有些问题。现在采用堆栈的方法,dfs到某一节点时将其入栈,当low[x]=x时不断出栈,知道将x出栈为止,因为x是这个强连通分量的根。
程序:
代码
1 var a,kl:array[1..1000,1..1000]of longint;
2 d,f,low,stack:array[1..1000]oflongint;
3 i,j,n,time,h:longint;
4
5 procedure dfs(x:longint);
6 var i:longint;
7 begin
8 inc(time);
9 d[x]:=time;
10 inc(h);
11 stack[h]:=x;
12 for i:=1 to n do
13 if a[x,i]>0 then
14 begin
15 if d[i]=0 then
16 begin
17 kl[x,i]:=1;
18 dfs(i);
19 end;
20 if (d[i]>0)and(f[i]=0) thenkl[x,i]:=2;
21 if f[i]>0 then
22 begin
23 if d[x]>d[i] then kl[x,i]:=3else kl[x,i]:=4;
24 end;
25 end;
26 inc(time);
27 f[x]:=time;
28 for i:=1 to n do
29 if a[x,i]>0 then
30 if(f[low[i]]=0)and(d[low[i]]<d[low[x]])and(d[low[i]]>0) then
31 low[x]:=low[i];
32 if low[x]=x then
33 begin
34 while stack[h]<>x do
35 begin
36 write(stack[h],' ');
37 dec(h);
38 end;
39 writeln(stack[h]);
40 dec(h);
41 end;
42 end;
43
44 begin
45
46 readln(n);
47 for i:=1 to n do
48 begin
49 for j:=1 to n do read(a[i,j]);
50 readln;
51 end;
52
53 fillchar(d,sizeof(d),0);
54 fillchar(d,sizeof(d),0);
55 for i:=1 to n do low[i]:=i;
56 time:=0;
57 fillchar(stack,sizeof(stack),0);
58 h:=0;
59 for i:=1 to n do
60 if d[i]=0 then dfs(i);
61
62 end.
有人会问,这里边的分类用不到啊?我会郑重地回答:就是没用。实际上,原因在于:BYVoid神牛的Blog里也讲到了Tarjan算法(图文并茂啊。。我就是看这个学的),而里面涉及了边的分类却没有给予解释,对于初学者(又是我?)不利。所以我就写了这些。
Jollwish原创,转载请说明出处

浙公网安备 33010602011771号