有向图的强连通分量算法

tarjan基于这样一个定理:
在任何深度优先搜索中,同一强连通支内的所有顶点均在同一棵深度优先树中。也就是说,强连通分量一定是有向图的某个深搜树子树。
证明:
在强连通支内的所有结点中,设r第一个被发现。因为r是第一个被发现,所以发现r时强连通支内的其他结点都为白色。在强连通支内从r到每一其他结点均有通路,因为这些通路都没有离开该强连通支(据引理1),所以其上所有结点均为白色(未染色,未访问)。因此根据白色路径定理,在深度优先树中,强连通支内的每一个结点都是结点r的后代。

引理:白色路径定理:
在一个有向或无向图G=(V,E)的深度优先森林中,结点v是结点u的后代当且仅当在搜索发现u的时刻d[u],从结点u出发经一条仅由白色结点组成的路径可达v。
证明:
→:假设v是u的后裔,w是深度优先树中u和v之间的通路上的任意结点,则w必然是u的后裔,由推论1 u的发现时刻d[u]一定比w的发现时刻早,因此在时刻d[u],w应为白色(未发现)。

←:设在时刻d[u],从u到v有一条仅由白色结点组成的通路,但在深度优先树中v还没有成为u的后裔。不失一般性,我们假定该通路上的其他顶点都是u的后裔(否则可设v是该通路中最接近u的结点,且不为u的后裔),设w为该通路上v的祖先,使w是u的后裔(实际上w和u可以是同一个结点)。根据推论1得f[w]≤f[u],因为v∈Adj[w],对DFS_Visit(w)的调用保证完成w之前先完成v,因此f[v]<f[w]≤f[u]。因为在时刻d[u]结点v为白色,所以有d[u]<d[v]。由推论1可知在深度优先树中v必然是u的后裔。(证毕)

引理:推论1 后裔区间的嵌入
在有向或无向图G的深度优先森林中,结节v是结点u的后裔当且仅当d[u]<d[v]<f[v]<f[u]。

(上述定理中,d[u]表示深搜过程中发现顶点的时间,f[u]表示对其深搜完毕的时间)

---------------
关于搜索树中的边

对有向图进行DFS, 得到深搜树,与无向图不同,无向图只包括 树边和回退边,而有向图的树包括四种:
若 dfn[v] < dfn[w] , (v,w) 是向前边或者树边。
若 dfn[v] > dfn[w] , (v,w) 是回退边或横跨边。

区分树边和向前边: visiteid[v] = true, visited[w] = false, (v,w)为树边。
visited[v] = true, visited[w] = true, 且 dfn[v] < dfn[w], (v,w)为向前边。
区分回退边和横边:
(数据结构书上的). 搜索过程中,每当树边(v,w)归入树边集时,记下j 的父亲 father[w] = v. 于是当遇上任意条边(v,w), 当visited[v] = true, visited[w] = true, 且dfn[v] > dfn[w] ,就由节点v 沿着树边根据father数组向上查找w, 如果找到了w, 说明是回退边,否则是横边。

横边有两种,一种是森林各个树之间的。一种是连通分量之内的。 当遇到横跨边,如果边的顶点还未归于一个被搜索完成的强连通分量, 那么该横边的点是可达的。否则是不可达的,即该边的顶点不可用。

tarjan 算法原理:

既然强连通分量是深搜树的一棵子树,要找到一个分量,只要找到树根( 连接不同分量的桥的前端顶点) ,然后取出其所属分量的顶点即可。

当深搜过程中遇到一个分量,首先发现的顶点v肯定是树根,由于分量是一棵子树,且分量间顶点可互达,那么其他顶点肯定能由v 发起的路径到达,那么其他节点都是v的子孙。
同无向图双连通分量的解决方式类似,我们定义 low 数组,low[v] 表示,从v出发经过v 的子孙形成的路径和一条回退边,能够到达的最浅层次的顶点的编号。注意这里到达的顶点必须是可用的,我们说当一个分量完成,其包括的顶点全部设置为不可用。其实如果边是一条不同子树之间的横跨边,它到达的顶点肯定是不可用的,因为那个点已经属于另一个分量。

在一个分量中,树根的子孙都不可能到达树根的祖先顶点。(容易证明:如果跟是u,某子孙v可达u的祖先w,由于w->u->v->w, 则w也属于该分量,且w应该是树根,矛盾)

在dfs过程中,遇到新点令其dfn[u] = low[u] = 编号. 回溯时,如果发现dfn[u] == low[u], 则是分量子树的树根。 这是一个充要条件。 -->: 如果u 是树根,且由于是强分量,其子孙都可达到u, 则子孙的low 都是u 的编号。则low[u] = dfn[u]. <--: 如果low[u] == dfn[u], 假设u不是树根,那么肯定有u的祖先是树根,那么low[u]肯定比当前值小,矛盾,因此u是树根。

如何取出分量? 我们遇到新顶点时将顶点入栈,在回溯时发现了根,且根是先被压入栈的,那么在根之后压入的顶点都是分量中的顶点,否则如果有顶点属于其他分量,就出现矛盾,因为在那个分量完成时,这个顶点已经弹出不可用了。

引用:来自pure_life----------------------------------------------------------
Trajan算法

1. 算法思路:

这个算法思路不难理解,由开篇第一句话可知,任何一个强连通分量,必定是对原图的深度优先搜索树的子树。那么其实,我们只要确定每个强连通分量的子树的根,然后根据这些根从树的最低层开始,一个一个的拿出强连通分量即可。那么身下的问题就只剩下如何确定强连通分量的根和如何从最低层开始拿出强连通分量了。

那么如何确定强连通分量的根,在这里我们维护两个数组,一个是indx[1..n],一个是mlik[1..n],其中indx[i]表示顶点i开始访问时间,mlik[i]为与顶点i邻接的顶点未删除顶点jmlik[j]mlik[i]的最小值(mlik[i]初始化为indx[i])。这样,在一次深搜的回溯过程中,如果发现mlik[i]==indx[i]那么,当前顶点就是一个强连通分量的根,为什么呢?因为如果它不是强连通分量的跟,那么它一定是属于另一个强连通分量,而且它的根是当前顶点的祖宗,那么存在包含当前顶点的到其祖宗的回路,可知mlik[i]一定被更改为一个比indx[i]更小的值。

至于如何拿出强连通分量,这个其实很简单,如果当前节点为一个强连通分量的根,那么它的强连通分量一定是以该根为根节点的(剩下节点)子树。在深度优先遍历的时候维护一个堆栈,每次访问一个新节点,就压入堆栈。现在知道如何拿出了强连通分量了吧?是的,因为这个强连通分量时最先被压人堆栈的,那么当前节点以后压入堆栈的并且仍在堆栈中的节点都属于这个强连通分量。当然有人会问真的吗?假设在当前节点压入堆栈以后压入并且还存在,同时它不属于该强连通分量,那么它一定属于另一个强连通分量,但当前节点是它的根的祖宗,那么这个强连通分量应该在此之前已经被拿出。现在没有疑问了吧,那么算法介绍就完了。


--------------强连通分量一定是图的深搜树的一个子树。
收缩有向图中的强连通分量大约是图论的线性算法中最具技巧性一种了。我们的首要目的是对于每个顶点设定一个Belong值,也就是它从属于哪个顶点所代表的强连通分量,至于重新建立图的边,不过是将所有边扫描一遍看是否在新图中出现而已,比较容易。
下面是利用一遍DFS求强连通分量的方法:对于每个顶点,设立Num、Low、Used、Alive四个属性,一个Stack保存当前正在被遍历的顶点;每访问一个顶点,将它的Num和Low设立为当前时间戳,Used、Alive设为True,并将顶点入栈,对于它的每条边,若边的终点未Used,则访问,而后用终点的Low值更新它的Low值,对于每个Used且Alive的终点,用它的Num值更新当前值;访问完毕当前顶点后,若Low与Num相等,说明找到了一个强连通分量,它包括栈中所有在当前顶点上方的顶点,将它们出栈并记下Belong值,出栈的顶点的Alive要置为false。
注意这里的Low值与求割顶和桥时的Low值的定义不太相同,它是与当前顶点双连通的最低顶点,所以才要用Alive来标记是否能到达当前顶点

-------------------------------------------------------引用-----------------------------END

我的完整代码:

1 #include <stdio.h>
2 #define MAX_VERTICES 50
3 #define true 1
4 #define false 0
5 #define MIN(x,y) ((x) < (y) ? (x) : (y))
6 typedef struct node *node_pointer;
7 struct node {
8 int vertex;
9 struct node *link;
10 };
11
12 node_pointer graph[MAX_VERTICES];
13
14 int n = 0;
15 int dfn[MAX_VERTICES];
16 int low[MAX_VERTICES];
17
18 int stk[MAX_VERTICES]; //辅助栈,存储dfs过程中访问的顶点
19 int stk_top = 0;
20 int alive[MAX_VERTICES]; //当顶点已经属于一个强连通分量,则不再活动alive[u]= false,否则alive[u] = true
21 int belong[MAX_VERTICES];// 每个顶点属于哪个顶点为根的强连通分量子树
22
23 int num = 0;
24
25
26
27 void addEdge(int v, int w) {
28 node_pointer e = (node_pointer)malloc(sizeof(struct node));
29 e->vertex = w;
30 e->link = graph[v];
31 graph[v] = e;
32 }
33
34 void addREdge(int v,int w){
35 addEdge(v,w);
36 addEdge(w,v);
37 }
38
39
40 void init() {
41 int i = 0;
42 num = 0;
43 n = 6; //0 to n
44 while(i<=n) {
45 graph[i] = 0;
46 dfn[i] = low[i] = -1;
47 alive[i] = true;
48 i++;
49 }
50
51 addEdge(0,1);
52 addEdge(1,2);
53 addEdge(2,3);
54 addEdge(3,1);
55 addEdge(2,4);
56 addEdge(4,2);
57 addEdge(0,5);
58 addEdge(5,2);
59 addEdge(5,6);
60 addEdge(6,0);
61
62 }
63
64 void tarjan_dfs(int u) {
65 node_pointer ptr;
66 int w,temp;
67 stk[stk_top++] = u;
68 low[u] = dfn[u] = num++;
69 for(ptr = graph[u]; ptr; ptr = ptr->link) {
70 w = ptr->vertex;
71 if(dfn[w] != -1) { //访问过,回退边或者强连通的横边
72 if(alive[w])
73 low[u] = MIN(low[u], dfn[w]);
74 } else {//未访问
75 tarjan_dfs(w);
76 low[u] = MIN(low[u], low[w]);
77 }
78 }
79
80 if(low[u] == dfn[u]) {//满足根节点条件
81 do {
82 temp = stk[--stk_top];
83 alive[temp] = false; //不再活动
84 belong[temp] = u; //该点属于哪个顶点代表的强连通子图
85 }while(temp != u);
86 }
87 }
88
89 void tarjan() {
90 int i;
91 num = 0;
92 for(i=0; i <= n; i++) {
93 if(dfn[i] < 0)
94 tarjan_dfs(i);
95 }
96 }
97
98
99 void tarjan_print() {
100 int i;
101 for(i = 0; i <= n;i++) {
102 printf("%d:%d ",i, belong[i]);
103 }
104 printf("/n");
105 }
106
107 int main(){
108 init();
109 tarjan();
110 tarjan_print();
111 getchar();
112 }
113

你可能感兴趣的:(算法)