虽然暂时用不到,还是花时间学习了一下,看网上玩ACM的大牛们都在做图论的题目,我也眼红了。。。
因为需要用到求强连通分量来判断AOE/PERT中的环路,先研究研究无向图的双连通分量。
对今天的学习做个总结:
无向图的连通分支(连通子图): 判断一个无向图是否连通,如果进行dfs或者bfs之后,还有未访问到的顶点,说明不是连通图,否则连通。
求解无向图的所有连通分支: 只需要重复调用dfs或者bfs 就可以解决:遍历顶点,如果v 未访问,则对其进行dfs, 然后标记访问。过程如下:
1 void dfs(int v){
2 node_pointer w;
3 visited[v] = TRUE;
4 for(w = graph[v]; w; w = w->link) {
5 if(!visited[w->vertex])
6 dfs(w->vertex);
7 }
8 }
9 void connect(){
10 int i;
11 for(i = 0; i < n; i++)
12 if(!visited[i]) {
13 dfs(i);
14 }
15 }
关节点(割点): 是图中一个顶点v, 如果删除它以及它关联的边后,得到的新图至少包含两个连通分支。
双连通图: 没有关节点的连通图。
连通无向图的双连通分支(双连通子图,块) : 是图G中一个最大双连通子图。
利用深度优先搜索dfs 可以求解双连通分支,因为dfs过程中,必定要经过关节点,并生成一棵深度优先搜索树。 而图G的连通子图必然是深搜树的一部分。
这张图很难看。 它有4个关节点:1,3,4,7, 将 图分为6个双连通分支。
如果以顶点3开始深搜,得到如下一棵树:
3是树根, 红色标号是 深度搜索访问节点的顺序, 红色边是图中深度搜索没有访问到的边(因为有的顶点可以多个边到达,深搜只要通过一个边到达顶点,就不再访问该顶点了),称作非树边,也就是树中没有的。 黑色的边是树边。
如果两个顶点u,v ,其中u是v的祖先或者v是u的祖先,那么非树边(u,v)叫做回退边。在深搜树中,所有的非树边都是回退边。 无向图的深搜树是一棵开放树,如果在其中添加一条回退边,就会形成环,该环路或扩大连通分量的范围,或者导致新的连通分量产生。
通过这个过程,可以发现一条规律:当v是树根,如果它有2个或者更多儿子,那么它是一个关节点。
当v不是树根,当且仅当它有至少一个儿子w, 且从w出发,不能通过w的后代顶点组成的路径和一条回退边到底u 的任意一个祖先顶点,此时v 是一个关节点。 其道理很明显,如果树根包含多个儿子,那么把根节点去掉,整棵树自然被分成多个不相干的部分,图也就断开了。如果v是非根顶点,如果其子树中的节点均没有指向v祖先的回边,那么去掉v以后,将会把v及其子树与图的其他部分分割开来,v自然是关节点。
例如顶点5,它的儿子只有6,而6 能到达的最低层顶点是5(通过 6->7->5), 无法访问到5的祖先顶点,因此5是一个关节点。
基于这样的规律,我们给每个顶点定义一个low值,low(u) 表示从u出发,经过一条其后代组成的路径和回退边,所能到达的最小深度的顶点的编号。( 如果这个编号大于等于u的编号,就说明它的后代无法到达比u深度更浅的顶点,即无法到达u的祖先,那么u就是个关节点)
low(u) = min{ dfn(u), min{ low(w) | w是u的儿子}, min{dfn(w), | (u,w) 是一条回退边} }
dfn(u) 是深搜过程中对顶点的编号值。
计算过程如下:
1 void dfnlow(int u, int v) {
2 node_pointer ptr;
3 int w;
4 dfn[u] = low[u] = num++;
5 for(ptr = graph[u]; ptr; ptr = ptr->link) {
6 w = ptr->vertex;
7 if(dfn[w] < 0) {
8 dfnlow(w, u);
9 low[u] = MIN(low[u], low[w]);
10 } else if( w != v)
11 low[u] = MIN(low[u], dfn[w]);
12 }
13 }
14
因此,我们在深搜过程中计算出 dfn 值和 low 值,如果发现 u有一个儿子w ,使得 low(w) >= dfn(u), 那么u就是关节点。
求解双连通分量的过程,可以通过深搜完成。 在搜索过程中,如果遇到一个新的边,则压栈,直到找到一个关节点,由于深搜是递归的,在找到一个关节点的同时,必定已经访问完了其子孙节点和其子树的边(包括回退边),而且这些边都在栈中,此时弹出栈中的边直到遇到关节点所在的边即是双连通分支包括的边。
完整代码:
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 typedef struct {
19 int v;
20 int w;
21 }edge;
22 edge edges[100];
23 int top = 0;
24
25
26 int num = 0;
27
28 void printG() {
29 int i;
30 node_pointer e;
31 for(i=0;i<=n;i++) {
32 printf("[%d]",i);
33 for(e=graph[i];e;e=e->link)
34 printf(" (%d)->",e->vertex);
35 printf("\n");
36 }
37 }
38
39 void printDfnLow() {
40 int i = 0;
41 while(i<=n) {
42 printf("[%d]: dfn:%d low:%d\n", i, dfn[i], low[i]);
43 ++i;
44 }
45 }
46
47
48 void addEdge(int v, int w) {
49 node_pointer e = (node_pointer)malloc(sizeof(struct node));
50 e->vertex = w;
51 e->link = graph[v];
52 graph[v] = e;
53 }
54 //无向图中一条边在邻接表中对应两个节点,1->2,2->1
55 void addREdge(int v,int w){
56 addEdge(v,w);
57 addEdge(w,v);
58 }
59
60
61 void init() {
62 int i = 0;
63 n = 9; //0 to n
64 while(i<=n) {
65 graph[i] = 0;
66
67 dfn[i] = low[i] = -1;
68 i++;
69 }
70
71 num = 0;
72
73
74 addREdge(3,5);
75 addREdge(5,7);
76 addREdge(5,6);
77
78 addREdge(6,7);
79 addREdge(7,9);
80 addREdge(7,8);
81 addREdge(0,1);
82 addREdge(1,2);
83 addREdge(1,3);
84 addREdge(2,4);
85 addREdge(4,3);
86
87 }
88
89 void dfnlow(int u, int v) {
90 node_pointer ptr;
91 int w;
92 dfn[u] = low[u] = num++;
93 for(ptr = graph[u]; ptr; ptr = ptr->link) {
94 w = ptr->vertex;
95 if(dfn[w] < 0) {
96 dfnlow(w, u);
97 low[u] = MIN(low[u], low[w]);
98 } else if( w != v)
99 low[u] = MIN(low[u], dfn[w]);
100 }
101 }
102
103 void bicon(int u, int v) {
104 node_pointer ptr;
105 int w;
106 edge e;
107 dfn[u] = low[u] = num++;
108
109 for(ptr = graph[u]; ptr; ptr = ptr->link) {
110 w = ptr->vertex;
111
112 if(v!=w && dfn[w] < dfn[u]) { //v!=w to avoid 1->2 2->1 in undirected graph
113 // dfn[w] < dfn[u] to avoid visited vertex who is decendant of u
114 edges[top].v = u; // 新边压栈,v!=w是防止重复计算无向图中同一条边
//dfn[w]<dfn[u] 是防止重复计算回退边,因为dfs过程中,
//遇到的顶点只有两种情况,dfn[w]=-1新点, dfn[w]<dfn[u]
//u,w 是回退边。二者的共同点是 dfn[w] < dfn[u],这两种
//边包括了G的所有边,因此对其他边的访问是重复的。
115 edges[top].w = w;
116 ++top;
117 if(dfn[w]< 0) { //如果是新顶点(未访问过)
118 bicon(w,u); //递归计算
119 low[u] = MIN(low[u], low[w]);// 更新当前u的low
120
121 if(low[w] >= dfn[u]) { //如果发现u的孩子w 满足条件,说明u是关节点
122 printf("New biconnected component:\n");
123 do{
124 e = edges[--top]; //此时栈中是上面的bicon压入的访问过的边,
//即该关节点下的子树边和回退边
125 printf("<%d,%d>", e.v, e.w);
126 }while( !(e.v == u && e.w == w));
127 printf("\n");
128 }
129 } else if (w!=v){
130 low[u] = MIN(low[u], dfn[w]);
131 }
132 }
133 }
134 }
135 int main(){
136 init();
137 printG();
138 //dfnlow(3,-1);
139 bicon(3,-1);
140 printDfnLow();
141 getchar();
142 }
143
注意在无向图深搜树中,只有两种边:树边(u->v u是v的父亲,v未访问)和回退边(u->v, v是u的祖先,v访问过)。 且无向图的边在邻接表中其实是"双向"的。因此我们要通过一些条件来只使用树边和回退边。
因此对于边u,v dfn[u] < dfn[v] && v 访问过 (即回退边的反向) 或者 dfn[u] > dfn[v], v 是u的父亲(树边的反向),这两种都是已经访问过的边,不需要重复访问。
我对回退边的理解是,它圈定了一个由关节点分割的连通子图的范围。
因为当遇到一个u的子孙 w, 使 low[w] >= dfn[u], 就说明 w 以及w 的子孙都无法访问到 u的祖先,那么去掉u 后,w 以及其子树就被和图的其他部分分割开来,形成了连通子图。这里的割点就是low[w] 能到达的最底层节点,也就是深搜过程中最靠近这个连通子图的关节点。
那么下界其实就是w. 因为设 v是w的孩子, low[v] < dfn[w] , 那么low[w] 肯定 等于 low[v], 从而 low[w] 小于本来的 low[w], 和前提矛盾。如果 low[v] >= dfn[w] , 则 w是一个关节点,那么w自然是一个界限,将刚才的连通子图和新生成的连通子图分开来,也就是前一个连通图的下界。