关节点算法
所谓关节点就是在一个连通图中删除某个顶点及其附属的边,原图变为非连通图。从任一点出发深度优先遍历得到优先生成树,对于树中任一顶点V而言,其孩子节点为邻接点。由深度优先生成树可得出两类关节点的特性:
(1)若生成树的根有两棵或两棵以上的子树,则此根顶点必为关节点。因为图中不存在连接不同子树顶点的边,若删除此节点,则树便成为森林。
(2)若生成树中某个非叶子节点V,其某棵子树与V的祖先节点无连接,则V为关节点。因为删去v,则其子树和图的其它部分被分割开来
用图说明一下吧:
如上图所示,很显然去掉A或C后整个图就变成不联通了,因此A、C就是关节点。怎么用代码实现找出关节点呢?其实我们可以严格按照上面的两条性质用dfs搜索树上来实现。首先,我们假设搜索顺序是A B C D E F,那么我们可以画出一棵搜索树:
我们先来看性质(1),根A有2棵子树分别为B,D-C-E-F。因此A必定为关节点,因为各个子树不可能存在一条到达A的祖先(A没有祖先)的回边构成一条回路。所以在以某一条边开始搜索到最后搜索完毕时,需要查看根的子树的个数,若子树小于2则该点不是关节点,反之则是关节点。
假如搜索顺序为:D C A B F E,当前节点是A,A的两棵子树分别为B,F-E;因为E-F子树存在一条回边c-e(用小写字母表示吧)导致A有一条可以到达祖先的回路,此时我们并不能确定A是不是关节点,因为还有一颗子树B没有检测,然而A并没有一条回边,当我们去掉A后,它的子树B就与它的祖先失去“联系”,因此A就是关节点。
那么我们怎么确定一条回边呢?我们可以在dfs的过程中将搜索序列号记录下来存进pre[]内,在搜索每一条路径结果若到达一个节点序号已经存在而且比自己的搜索序列号小,我们可以将该序列号记录下来存进low[]中作为某一个节点通过某一条路径所能到达的最小序列值。在每一个子树回溯给该节点所能到达的最小值时,我们先判断该值是否小于该节点的序列号,若小于,那么该节点可以通过该子树到达自己的祖先,我们在将该节点的low[]更新。否则,该子树是一棵真正存在于该节点下的一颗子树。
总结以上陈述也就是说,搜索每一个节点通过自己的各个子树所能到达的最小序列号是否都比自己的序列号小,若成立则是关节点,否则不是。
若以临接表作为数据结构,代码可以写成如下形式:
int dfs(int i,int root)//但前节点及其父节点
{
int j,w,k=0;
if(pre[i]==0) pre[i]=low[i]=++cnt;
for(j=head[i];j;j=edge[j].next)
{
w=edge[j].i;
if(pre[w]==0)
{
dfs(w,i);
if(low[i]>low[w]) low[i]=low[w];//更新
if(low[w]>=pre[i]) k++;
}
else if(w!=root)
{
if(low[i]>pre[w]) low[i]=pre[w];
}
}
if(k) count++;//关结点个数加一
return k;
}
for(i=1;i<=n;i++)
{
if(pre[i]==0)
{
k=dfs(i,0);
if(k<2) count--;
}
}
为什么if(low[w]>=pre[i]) k++; 中需要用k++呢,为什么搜索接收后有一个if(k<2) count--;
if(low[w]>=pre[i]) k++;中的k表示有几个不能够到达祖先的子树,有人说将k可变为bool变量,其实k的值在搜索过程中确实可以换成bool,但是搜索结束后,我们需要用到这个值,这也是性质(1)决定的,根节点的所有子节点序列值不可能小于根节点,因此搜索完毕后根节点肯定作为一个关节点来算,因此我们需要重新检测一下根节点到底有几棵子树,此时k只就派上用场了。
有人会在if(low[w]>=pre[i])犯糊涂,为什么是>=而不是>呢?那好我们看下下面一个图:
假如搜索序列是A B C E D,而且if(low[w]>pre[i]),我们画出搜索树:
若当前节点为B,则B有两棵子树C,E-D;B可以通过子树C到达祖先A,而B通过子树E-D所获的最小序列号是B的序列号,若按if(low[w]>pre[i])那么B就不是关节点,这就错了。对于根A来说它只有一棵子树,当然不是关节点。有人会说拓扑图是如下这样,若按照if(low[w]>=pre[i])不也错了吗。
其实不然,假如说搜索顺序为A B C D ;我们之前说过整个搜索完成之后对根做一次再检测,显然A不会是关节点。