关节点和重连通分量
// algo7-3.cpp 实现算法7.10、7.11的程序 #include"c1.h" #define MAX_NAME 2 // 顶点字符串的最大长度+1 typedef int InfoType; typedef char VertexType[MAX_NAME]; // 字符串类型 #include"c7-21.h" // 邻接表存储结构 #include"bo7-2.cpp" // 邻接表的基本操作 int count,lowcount=1; // 全局量count对访问顺序计数,lowcount对求得low值的顺序计数 int low[MAX_VERTEX_NUM],lowOrder[MAX_VERTEX_NUM]; // 全局数组,low[]存顶点的low值,lowOrder存顶点求得low值的顺序 void DFSArticul(ALGraph G,int v0) { // 从第v0个顶点出发深度优先遍历图G,查找并输出关节点 int min,w; ArcNode *p; visited[v0]=min=++count; // v0是第count个访问的顶点,visited[]是全局变量,在bo7-2.cpp中定义,min的初值为v0的访问顺序 for(p=G.vertices[v0].firstarc;p;p=p->nextarc) // 依次对v0的每个邻接顶点检查 { w=p->data.adjvex; // w为v0的邻接顶点位置 if(visited[w]==0) // w未曾访问,是v0的孩子 { DFSArticul(G,w); // 从第w个顶点出发深度优先遍历图G,查找并输出关节点。返回前求得low[w] if(low[w]<min) // 如果v0的孩子结点w的low[]小,这说明孩子结点还与其它结点(祖先)相邻 min=low[w]; // 取min值为孩子结点的low[],则v0不是关节点 else if(low[w]>=visited[v0]) // v0的孩子结点w只与v0相连,则v0是关节点 printf("%d %s\n",v0,G.vertices[v0].data); // 输出关节点v0 } else if(visited[w]<min) // w已访问,则w是v0在生成树上的祖先,它的访问顺序必小于min min=visited[w]; // 故取min为visited[w] } low[v0]=min; // vo的low[]值为三者中的最小值 lowOrder[v0]=lowcount++; // 记录v0求得low[]值的顺序(附加),总是在返回主调函数之前求得low[] } void FindArticul(ALGraph G) { // 连通图G以邻接表作存储结构,查找并输出G上全部关节点。全局量count对访问计数。算法7.10 int i,v; ArcNode *p; count=1; // 访问顺序 visited[0]=count; // 设定邻接表上0号顶点为生成树的根,第1个被访问 for(i=1;i<G.vexnum;++i) visited[i]=0; // 其余顶点尚未访问,设初值为0 p=G.vertices[0].firstarc; // p指向根结点的第1个邻接顶点 v=p->data.adjvex; // v是根结点的第1个邻接顶点的序号 DFSArticul(G,v); // 从第v顶点出发深度优先查找关节点 if(count<G.vexnum) // 由根结点的第1个邻接顶点深度优先遍历G,访问的顶点数少于G的顶点数 { // 说明生成树的根有至少两棵子树,则根是关节点 printf("%d %s\n",0,G.vertices[0].data); // 根是关节点,输出根 while(p->nextarc) // 根有下一个邻接点 { p=p->nextarc; // p指向根的下一个邻接点 v=p->data.adjvex; if(visited[v]==0) // 此邻接点未被访问 DFSArticul(G,v); // 从此顶点出发深度优先查找关节点 } } } void main() { int i; ALGraph g; printf("请选择无向图\n"); CreateGraph(g); // 构造无向图g Display(g); // 输出无向图g printf("输出关节点:\n"); FindArticul(g); // 求连通图g的关节点 printf(" i G.vertices[i].data visited[i] low[i] lowOrder[i]\n"); // 输出辅助变量 for(i=0;i<g.vexnum;++i) printf("%2d %9s %14d %8d %8d\n",i,g.vertices[i].data,visited[i],low[i],lowOrder[i]); }
代码的运行结果:
请选择无向图
请输入图的类型(有向图:0,有向网:1,无向图:2,无向网:3): 2(见图759)
请输入图的顶点数,边数: 13,17
请输入13个顶点的值(<2个字符):
A B C D E F G H I J K L M
请输入每条弧(边)的弧尾和弧头(以空格作为间隔):
A B
A C
A F
A L
B C
B D
B G
B H
B M
D E
G H
G I
G K
H K
J L
J M
L M
无向图(见图760)
13个顶点:
A B C D E F G H I J K L M
17条弧(边):
A→L A→F A→C A→B
B→M B→H B→G B→D B→C
D→E
G→K G→I G→H
H→K
J→M J→L
L→M
输出关节点:
6 G
1 B
3 D
1 B
0 A
i G.vertices[i].data visited[i] low[i] lowOrder[i]
0 A 1 0 0 (没求A的low[])
1 B 5 1 9
2 C 12 1 8
3 D 10 5 7
4 E 11 10 6
5 F 13 1 12
6 G 8 5 3
7 H 6 5 5
8 I 9 8 2
9 J 4 2 1
10 K 7 5 4
11 L 2 1 11
12 M 3 1 10
运行algo7-3.cpp 构造的深度优先生成树如图761 所
示。图761 和图759 的拓扑结构是一样的。5 条用虚线
表示的边是构造深度优先生成树多余的边,它们是连接祖先
的回边。如果一个结点不仅有连向双亲的边,还有连向祖先
的回边。则对这个结点来说,它的双亲结点不是关节点。如
图761 中的结点B,它有连向双亲结点M 的边,还有连向
祖先结点A 的边。这样,如果删除结点M,B 仍然与图的其
它部分连通(重连通)。而图761 中的结点I,它只有连向双
亲结点G 的边。一旦结点G 被删除,结点I 就与图的其它部
分不连通,也就是一个连通分量被分割成了多个连通分量。
结点G 被称为关节点。
如何确定关节点?算法7.10、7.11 的思路是这样的:首
先在深度优先遍历图时,不仅标注某顶点是否被访问,还标
注它的访问顺序。visited[]不再只是FALSE 和TRUE,而是1~顶点数。由于采用深度优先
遍历,某结点的祖先被访问的顺序必先于该结点被访问的顺序。仍以图761 为例,由第
1 个结点A 深度优先遍历的顺序是:A、L、M、J、B、⋯⋯ 增加1 个辅助数组low[],
对顶点v,定义low[v]=min(visited[v],low[w],visited[k])。其中w 和k 分别是v 的孩子
和由回边相连的祖先。由算法7.11 可知,low[]是在递归调用返回之前求得的。所以,求
得low[]的顺序是:⋯⋯ B、M、L、⋯⋯ 也就是说,孩子的low[]是先于双亲的low[]而
获得的。这可由程序运行结果中的lowOrder[]看出(增加辅助数组lowOrder[]的目的就是
帮助分析求得low[]的顺序)。
如果顶点v 有孩子w,且有low[w]≥visited[v],则顶点v 必为关节点。下面分析几
种可能存在的情况:
(1) 如果顶点v 有通过回边相连的祖先k,则low[v]=visited[k](祖先顶点k 被访问的
顺序)。同时k 也是v 的双亲u 的祖先或双亲,故有low[v]=visited[k]<visited[u](结点祖
先或双亲必先于该结点被访问)。不满足判定关节点的公式,故u 不是v 的关节点。这种
情况如图761 中顶点B 的low[]=顶点A 的visited[]=1,其双亲M 的visited[]=3,故M
不是B 的关节点。
(2) 如果顶点v 没有通过回边相连的祖先,但有孩子w,而孩子顶点w 有通过回边相
连的祖先k,则low[w]=visited[k],而k 也是v 的双亲u 的祖先,仍有visited[k]≤
visited[u]。如顶点K 没有通过回边相连的祖先,但有孩子G,而G 有通过回边相连的祖
先B。顶点G 的low[]等于顶点B 的visited[]=5,也等于顶点K 的low[]。而K 的双亲H
的visited[]=6,故H 不是K 的关节点。
(3) 如果顶点v 既无孩子又无通过回边相连的祖先,则其双亲结点u 是关节点。在这
种情况下,low[v]=visited[v](顶点v 被访问的顺序)。而u 被访问的顺序必定小于v 的,
故有low[v]=visited[v]>visited[u]。所以u 是v 的关节点。如顶点E 就是既无孩子又无通
过回边相连的祖先,则其双亲结点D 是E 的关节点。
(4) 如果顶点v 没有通过回边相连的祖先,虽有孩子顶点w,但w 也没有通过回边相
连的祖先,则v 的双亲结点u 是关节点。在这种情况下,low[w]= visited[w](顶点w 被访
问的顺序)>low[v]=visited[v](顶点v 被访问的顺序)>visited[u],故u 是v 的关节点。如
顶点D 虽有孩子顶点E,但E 没有通过回边相连的祖先,则low[D]=5=visited[B]。故B
是D 的关节点。
通过low[w]≥visited[v]来判断连通图关节点的方法不能用于根结点。因为根结点的
visited[]=1,是最小值。判断根结点是否为关节点要看它有几棵子树,如果超过1 棵,则
根结点就是关节点。原因是,它的每棵子树上的结点都和其它子树的不相连。否则在深度
优先遍历其它子树时,就会遍历到,也就不成为根结点的子树了。所以算法7.10 在深度
优先遍历时,不是直接从根结点遍历,而是从根结点的第1 个邻接顶点开始遍历。当遍历
完这个邻接顶点的生成子树,若还有顶点没被访问,则说明根结点是关节点。如图761
所示,对根结点A 的第1 棵子树L 遍历结束后,A 还有邻接点F 没被访问到。说明除根结
点A 之外,L 子树上的任何一个结点都不和F 邻接。这样,若根结点A 被删除,原图就会
被分割成L 子树和F 两部分。故根结点A 是关节点。
运行algo7-3.cpp 在输出关节点时,B 被输出了2 次。其原因是删除B 使连通图分割
成3 个连通分量。
每当夜深人静的时候,想想今天发生了什么,失去了什么,得到了什么,做了什么,没做什么,该做什么,不该做什么,明天要做什么!