先转载一篇好文章...
这张图很难看。 它有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自然是一个界限,将刚才的连通子图和新生成的连通子图分开来,也就是前一个连通图的下界。
自己的理解:
对于双连通分量的求法,与强连通分量的tarjan算法有类似的地方,关键是理解搜索树中回退边的意义。对于无向图,不用像有向图那样用一个color数组来表示某个点有没有搜索完,因为边是无向的,只要能不通过父亲从搜索树上走到已经访问过的节点,则必然能构成一个双连通分量。双连通分量对于一些无向图上的问题,作用与有向图的强连通类似:缩点。同时,也是求关节点(割点)和桥的算法。
两道pku上面的题目,pku3352和pku3177,题意是一样的。给出一个连通的无向图,问最少加入多少条边能使该图变成一个双连通无向图。唯一不同的是,后者有重边。
解题思路是一样的。将图中的双连通分量缩成一个点,则原图变成一棵树,树中的边均为原图中的桥,要使得树中没有桥,必须在所有的叶子节点(度为1的点)之间两两连边,所以答案为叶子节点数(cnt+1) div 2。
对于重边:如果当前节点的父节点被第二次访问,则不将其视为回退边而视为数边对其进行遍历。可以认为,该节点是从其他路径走回父节点,而非父节点走到儿子节点时所走的路径,故应该视为正常遍历到的节点。
代码(pku3177):