无向图的割点、桥与双连通分量

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/tham_/article/details/72353226

概念

对于无向图G,删除顶点v 和其相连的边后G所包含的连通分量增多,则称v关节点 (articulation point) 或割点 (cut point)。同理,删除边e 和其相连的顶点后图包含的连通分量增多,则e 割边 (cut edge) 或 (bridge)。

割点形式化的定义:A 是割点当且仅当存在两个点u,v 使得u v 的每条路径都会经过A (去掉A 后,u v没有路径)。

不含任何割点的图称为双连通图。任一无向图都可视作若干个极大双连通子图组合而成,每一个子图称为双连通分量 (bi-connected component)。

双连通分量的定义:是极大双连通子图,即如果G是双连通分量,则不存在G,使得GG的子图且G也是双连通分量。

点双连通分量:在连通分量中去掉一个顶点,连通分量仍连通。特殊的,点双连通分量又叫做

边双连通分量:在连通分量中去掉一条边,连通分量仍连通。

等价关系 (equivalence relation):边e1e2是等价关系当且仅当e1=e2e1e2在一个回路中。

等价类 (equivalence class):一个等价类中的两条边都是等价关系。

重要性

相对于图中其他顶点,割点更为重要。在网络系统中相当于网关,决定子网之间能否连通。在航空网络中,某些枢纽机场的损坏,可能导致与其他机场之间交通的中断。故在资源总量有限的情况下,应该找出图中的割点予以保障,这是提高系统整体稳定性和鲁棒性的基本策略。

一、割点、割边、双连通分支概念
挂接点(Articulation point)就是割点(Cut Vertex)
桥(Bridge)就是割边(Cut Edge)
割点:v为割点,则去掉v后,图的连通分支增加。
割边:v为割边,则去掉v后,图的连通分支增加。
割点形式化的定义:a是割点当且仅当存在两个点u,v使得u到v的每条路径都会经过a。(去掉a后,u到v没有路径)
边双连通分支:在连通分支中去掉一条边,连通分支仍连通。
点双连通分支:在连通分支中去掉一个点,连通分支仍连通。
我们这里说的是点双连通分支,因为由定义“任何两条边都在一个公共简单回路,且在一个双连通分支中没有割点,因此是点双连通分支。”。
割点应用场景:
给定一个计算机网络,如果有一个计算机v坏掉了,那么是否任何两个计算机都能够仍然连通?
遇到上述问题,是否能够转化成图论问题呢?
其实这个问题就是看坏掉的计算机是否是割点,如果是割点,则一定存在两个计算机u、v,u和v不连通。
双连通图定义:不存在割点。
双连通分支定义:他是极大双连通子图,就是如果G是双连通分支,则不存在G',G是G‘的子图,且G’也是双连通分支。

一些命题的证明
命题:两个双连通分支之间最多有一个公共点。
证明:假设双连通分支C1、C2,且有两个公共点v1、v2,因为双连通分支划分非桥边,所以v1与v2之间至少有两条边,因此假设v1与v2有两条边,如下图:


首先声明,e1、e2可能是个路径,即由多条边组成,但是我们假设e1与e2为边。
e1在C1中,因此e1与C1中任何一条边都在一个简单回路中,同理,e2与C2中任何一条边都在一个简单回路,因为e1与e2在一个回路,所以e1与C2中任何一条边都在一个简单回路,e2与C1中任何一条边都在一个简单回路,所以C1与C2合并。
命题:两个双连通分支之间的一个公共点是割点。
证明:如果公共点不是割点,则将A点去除后,C1与C2仍然连通,因此必然存在u与v使得(u,v)属于E,因此当A存在时,u到A存在路径,A到v存在路径,u和v之间存在一条边,因此(u,v)与C1中任何一条边都在一个简单回路,(u,v)与C2中任何一条边都在一个简单回路,所以C1与C2合并。
命题:割点一定属于至少两个双连通分支。
证明:如果割点属于一个双连通分支,根据双连通分支定义,去掉任何一个点都不会让图不连通,与割点的定义矛盾。
命题:在一个双连通分支中至少要删除两个点才能够使图G不连通。
证明:双连通分支定义。Trivial。
Equivalence relation:边e1和e2是等价关系的当且仅当e1=e2或e1与e2在一个回路中。
Equivalence Class:一个等价类中的两条边都是等价关系。
命题:对于根顶点u,u为割点当且仅当u有至少两个儿子。
首先说明一点,无向图只有树边和反向边。
=>
已知Gn的根是割点,如果Gn中该根s只有一个子女节点t,则去除了根节点后,其子女节点t仍然连接着分支,这与根s为割点条件不符,因此根至少两个子女。
<=
如果Gn中根有至少两个子女,因为无向图只存在树边、反向边,没有交叉边,因此当根去除后,分支之间不能连接,因此根为割点。
命题:对于非根顶点u,u为割点当且仅当u只要存在一个子顶点s,s的后裔(注意是后裔,不是真后裔)没有指向u的真祖先的反向边。
=>
已知Gn中某个非根节点v是割点,如果任何v的子顶点s,s或s的后裔指向v的真祖先的反向边,我们考虑一个分支,假设反向边为(a,b),其中a为v的子顶点,b为v的真祖先,当v删除后,因为v的真祖先连通,v的子顶点之间连通,如果存在{a,b}的边,则v的真祖先和v的子顶点也连通,与条件矛盾。
<=
如果存在一个子顶点s,不存在s或s的后裔指向v的真祖先的反向边,则v的真祖先区域和v的子顶点区域不连通,因此v为割点。
命题:对于非根顶点u为割点,当且仅当存在相邻子顶点,使得 low[v]>=d[u].
证明:
=>
因为u为割点,因此u有一个子顶点v,不存在v或v的真后裔顶点指向u的真祖先的反向边。
因为 low[v] = min { d[v], d[w]},其中对于v的后裔u,(u,w)为反向边。
因为d[w]>=d[u],所以low[v]>=d[u].
<=
因为low[v]>=d[u],所以 low[v] = min { d[v], d[w]}>=d[u],因为d[v]>=d[u],所以d[w]>=d[u],所以w为u的后裔。
所以不存在v,v或v的子顶点存在指向u的真祖先的反向边,因此u为割点
命题:(u,v)是桥,当且仅当low[v]>d[u].

证明:
=>
已知(u,v)是桥,则此边不在任何简单回路中,不失一般性,先访问u,再访问v,则low[v]=min { d[v], d[w]}, d[w]>d[u],所以low[v]>d[u].
<=
已知low[v]>d[u],所以d[w]>d[u],所以不存在v或v的子顶点,(v,w)为反向边,且w是u的后裔,所以(u,v)不属于简单回路,所以是桥。
命题:双连通分支的任意两条边位于同一个简单回路等价于双连通分支中没有割点。
证明:
如果存在割点u,设与u相邻的点为v1,v2,....vn,则因为u去掉后,应该使得v1...vn中的至少其中一个顶点和u不连通,假设此点为vi,但是根据双连通分支的定义,(u,vi)位于简单回路,因此根据定义u和vi应该仍然连通,所以矛盾。

查找点双连通分量算法

给定无向图G,如何确定割点和点双连通分量?

实际上,在求割点的过程中就能顺便把每个点双连通分量求出。

由命题5、命题6可知,对于一颗DFS搜索树中根顶点和内定点为割点的充分必要条件。其中重要的就是判断某一顶点有无子树与该顶点的真祖先通过反向边相连。那么,我们可以通过比对HCA (highest connected ancestor) 来记录可(经反向边)连接的最高祖先,然后通过比较每个顶点被搜索的开始时间start_time,判断哪个祖先越高(开始时间越小说明节点高度越高)。

以下代码在图的DFS基础上进行修改:

void BCC(int v, int &clock, stack<int> &s) {  
    hca(v) = start_time(v) = ++clock;
    status(v) = DISCOVERED;  // 发现v
    s.push(v);               // 顶点v入栈,枚举其所有邻居
    for (int u = first_neighbor(v); u > -1; u = next_neighbor(v, u)) {
                             // 当u没有邻居时,next_neighbor返回-1
        switch (u) {         // 视u的情况进行处理
        case UNDISCOVERED:   // 若u未被搜索,则(v, u)为树边,从顶点u开始继续深入递归搜索
            parent(u) = v;
            type(v, u) = TREE;
            BCC(u, clock, s);
            if (hca(u) < start_time(v)) {
                             // 递归返回后,若发现u可达的祖先比v还高,则说明u可以通过反向边连接v的真祖先
                hca(v) = min(hca(v), hca(u));
                             // 对v而言亦如此
            }
            else {           // 否则,v为割点,u以下即为一个BCC,且其中顶点集中于栈s的顶部
                while (v != s.top()) {
                    s.pop(); // 依次弹出当前BCC中的节点,亦可根据实际需求转存至其他结构
                }
                s.push(v);   // 最后一个顶点(割点)重新入栈,均摊下来不足1次
            }
            break;
        case DISCOVERED:
            type(v, u) = BACKWARD;
                             // 标记(v, u),并按照“越小越高”的准则
            if (u != parent(v)) {
                hca(v) = min(hca(v), start_time(u));
                             // 更新hca(v)
            }
            break;
        default:             // 对于无向图,剩下一种情况是已经访问(VISITED)
            type(v, u) = (start_time(v) < start_time(u)) ? FORWARD : CROSS;
            break;
        }
    }
    status(v) = VISITED;     // 结束对v的访问
    return;
}

由于访问的是无向图,DFS搜索在vv的子节点uu返回后通过比较hca(u)start_time(v)的大小关系即可判断v是否割点。当hca(u)start_time(v)时,uu和其后代无法通过反向边与v的真祖先联通。既然栈s存放了搜索过的顶点,则此时与该割点对应的双连通分量内的顶点应该集中在s的顶部,因此可以依次弹出它们。v作为多个联通分量的连接枢纽,在弹出以后应该重新进栈。

hca(u)<start_time(v),则说明uu可以经过反向边连接v的真祖先,这一规则对v同样适用,则有必要将hca(v)进行更新。当然,每遇到一条反向边(v,u),也需要及时的更新hca(v),以保证hca(v)始终能记录顶点v经反向边向上连通到的最高的祖先。

复杂度分析

与基本的DFS相比,此处只增加了一个规模为O(n)的辅助栈,故整体空间复杂度没有变化,仍然为O(n+e)

对于时间复杂度,同一顶点v可能多次入栈,但是每一次重复入栈都对应于发现新的双连通分量,因此必有至少另一顶点出栈并且不再入栈,故这类重复入栈操作不会超过n次,则入栈操作不会超过2n次,故算法整体复杂度依然是O(n+e)


猜你喜欢

转载自blog.csdn.net/tham_/article/details/72353226