zoukankan      html  css  js  c++  java
  • 图的割点、桥和双连通分支的基本概念

    点连通度与边连通度

    回到正题,首先介绍下什么是图的边连通度和点连通度。一般来说,点连通度是指对应一个图G,对于所有点集U属于V(G),也就是V(G)的子集中,使得G-U要么是一个非连通图,要么就是一个平凡图(即仅包含一个独立点的图),其中最小的集合U的大小就是图G的点连通度,有时候也直接称为图的连通度。通俗点说,就是一个图G最少要去掉多少个点会变成非连通图或者平凡图。当然对于一个完全图来说Kn来说,它的连通度就是n-1。
    同理,边连通度就是对于一个非平凡图G,至少去掉多少条边才能使得该图变成非连通图。我们的问题就是,对于任意一个图,如何求该图的连通度以及边连通度?这跟最大流问题有什么联系?
    简单起见,我们先说如何求一个图的边连通度lamda(G)。(基于无向图考虑)
    对于图G,设u,v是图G上的两个顶点,定义r(u,v)为删除最少的边,使得u到v之间没有通路。将图G转换成一个流网络H,u为源点,v是汇点,边容量均为1,那么显然r(u,v)就是流网络的最小割,根据(二)里的介绍,其等于流网络的最大流。
    但是,目前为止我们还没解决完问题,因为显然我们要求的边连通度lamda(G)是所有的点对<u,v>对应的r(u,v)中最小的那个值。这样的话我们就必须遍历所有的点对,遍历的的复杂度为O(n*n)。这显然代价太高,而事实上,我们也不必遍历所有点对。

    在这里插入图片描述
    如图所示,设S为图G的最小割集,那么lamda(G)=|S|。设在取任意一个点u,若u在L内,那么必然至少存在一个点v,使得r(u,v)=|S|(v是在R内时即成立)。所以,我们只需要任取一个点u,计算u和其他点的r(u,v),取最小者,必然是等于最小割集,即边连通度。

    双连通图、割点与桥

    点连通度与边连通度:
    在一个无向连通图中,如果有一个顶点集合v,删除顶点集合v,以及与v中顶点相连 (至少有一端在v中)的所有边后,原图不连通,就称这个点集v为割点集合。
    一个图的点连通度的定义为:最小割点集合中的顶点数。
    类似的,如果有一个边集合,删除这个边集合以后,原图不连通,就称这个点集为割边 集合。
    一个图的边连通度的定义为:最小割边集合中的边数。

    双连通图、割点与桥:
    如果一个无向连通图的点连通度大于1,则称该图是点双连通的 (point biconnected),简 称双连通或重连通。一个图有割点,当且仅当这个图的点连通度为1,则割点集合的唯一元素被称为割点(cut point),又叫关节点(articulationpoint)。一个图可能有多个割点。
    如果一个无向连通图的边连通度大于1,则称该图是边双连通的 (edge biconnected),简 称双连通或重连通。一个图有桥,当且仅当这个图的边连通度为 ,则割边集合的唯一元素 被称为桥(bridge),又叫关节边(articulationedge)。一个图可能有多个桥。
    可以看出,点双连通与边双连通都可以简称为双连通,它们之间是有着某种联系的,下 文中提到的双连通,均既可指点双连通,又可指边双连通。(但这并不意味着它们等价)。

    双连通分量(分支)
    在图G的所有子图G’中,如果G’是双连通的,则称G’为双连通 子图。如果一个双连通子图G’它不是任何一个双连通子图的真子集,则 为极大双连通子 图。双连通分量(biconnectedcomponent),或重连通分量,就是图的极大双连通子图。特殊的,点双连通分量又叫做块。

    Tarjan算法
    与有向图求强连通分量的Tarjan算法类似,只需通过求dfn值与low值来得出割点与桥。
    对图深度优先搜索(DFS),定义dfn(u)为u在搜索树 (以下简称为树)中被遍历到的次序号。定义low(u)为u或u子树中的结点经过最多一条后向边能追溯到的最早的树中结点次序号(注意:与DAG不同的是,这里的后向边不包括与搜索树中父亲的连边)。
    根据定义,则有:
    low(u)=Min{}dfn(u)dfn(v)(u,v)为后向边(返祖边)(一定注意不包括与父节点的连边)low(v)(u,v)为树枝边low(u)=Min{dfn(u)dfn(v)(u,v)为后向边(返祖边)(一定注意不包括与父节点的连边)low(v)(u,v)为树枝边}
    一个顶点u是割点,当且仅当满足 (1)或(2):
    (1)u为树根,且u有多于一个子树。因为无向图DFS搜索树中不存在横叉边,所以若有多个子树,这些子树间不会有边相连。
    (2)u不为树根,且满足存在(u,v)为树枝边 (即 为 在搜索树中的父亲),并使得 DFN(u)<=Low(v).(因为删去 后 以及 的子树不能到达 的其他子树以及祖先)。

    实现时,因为有重边的问题,所以需要将一条无向边拆为两条编号一样的有向边,用邻 接表进行存储。在判断 是否为后向边时要注意是树枝边的反向边还是新的一条反向边。

    求双连通分量
    下面要分开讨论点双连通分量与边双连通分量的求法。

    对于点双连通分量,实际上在求割点的过程中就能顺便把每个点双连通分支求量。建立 一个栈,存储当前双连通分量,在搜索图时,每找到一条树枝边或后向边(非横叉边),就 把这条边加入栈中。如果遇到某时满足 DFN(u)<=Low(v) 说明u是一个割点,同时把边从栈 顶一个个取出,直到遇到了边(u,v) ,取出的这些边与其相连的点,组成一个点双连通分量。割点可以属于多个点双连通分量,其余点和每条边只属于且属于一个点双连通分量。
    (这里选择储存边而不是储存点是因为一个割点可以属于多个点双连通分量)

    对于边双连通分量,求法更为简单。只需在求出所有的桥以后,把桥边删除,原图变成了多个连通块,则每个连通块就是一个边双连通分量。桥不属于任何一个边双连通分量,其 余的边和每个顶点都属于且只属于一个边双连通分量。可以用并查集实现。 (一定注意考虑重边的可能性)

    一个有桥的连通图,如何把它通过加边变成边双连通图?

    方法为首先求出所有的桥,然 后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。
    统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf 。则至少在树上添加 (leaf+1)/2条边,就能使树达到边二连通,所以至少添加的边数就是(leaf+1)/2。
    (证明略,请读者感性思考。。。)

    双连通分支

    在图G的所有子图G’中,如果G’是双连通的,则称G’为双连通子图。如果一个双连通子图G’它不是任何一个双连通子图的真子集,则G’为极大双连通子图。双连通分支(biconnected component),或重连通分支, 就是图的极大双连通子图。特殊的,点双连通分支又叫做块。

    求割点与桥

    该算法是R.Tarjan发明的。对图深度优先搜索,定义DFS(u)为u在搜索树(以下简称为树)中被遍历到的次序号。定义Low(u)为u或u的子树中能通过非父子边追溯到的最早的节点,即DFS序号最小的节点。根据定义,则有:Low(u)=Min{DFS(u)DFS(v)(u,v)为后向边(返祖边)等价于DFS(v) < DFS(u)且v不为u的父亲节点Low(v)(u,v)为树枝边(父子边)}一个顶点u是割点,当且仅当满足(1)或(2)(1)u为树根,且u有多于一个子树。(2)u不为树根,且满足存在(u,v)为树枝边(或称父子边,即u为v在搜索树中的父亲),使得 DFS(u) <= Low(v)。一条无向边(u,v)是桥,当且仅当(u,v)为树枝边,且满足DFS(u) < Low(v)。

    
    void Tarjan(int u, int father) //father 是u的父节点
    {	
    	Father[u] = father;
    	int i,j,k;
    	low[u] = dfn[u] = nTime ++;
    	for( i = 0;i < G[u].size() ;i ++ ) {
    		int v = G[u][i];
    		if( ! dfn[v]) {
    			Tarjan(v,u);
    			low[u] = min(low[u],low[v]);
                            if(low[u]>low[u]) u,v为桥
     }
    		else if( father != v ) //连到父节点的回边不考虑,否则求不出桥
    			low[u] = min(low[u],dfn[v]);
    	}
    }
    void Count()
    { //计算割点和桥
        int nRootSons = 0;    int i;
        Tarjan(1,0);
        for( i = 2;i <= n;i ++ ) {
            int v = Father[i];
            if( v == 1 )
                nRootSons ++; //DFS树中根节点有几个子树
            else {
                if( dfn[v] <= low[i])
                    bIsCutVetext[v] = true;
            }
        }
        if( nRootSons > 1)
            bIsCutVetext[1] = true;
        for( i = 1;i <= n;i ++ )
            if( bIsCutVetext[i] )
                cout << i << endl;
        for( i = 1;i <= n;i ++) {
            int v = Father[i];
            if(v >0 &&  dfn[v] < low[i])
                cout << v << "," << i <<endl;
        }
    }
    

    求双连通分支

    下面要分开讨论点双连通分支与边双连通分支的求法。
    对于点双连通分支,实际上在求割点的过程中就能顺便把每个点双连通分支求出。建立一个栈,存储当前双连通分支,在搜索图时,每找到一条树枝边或后向边(非横叉边),就把这条边加入栈中。如果遇到某时满 足DFS(u) <= Low(v),说明u是一个割点,同时把边从栈顶一个个取出,直到遇到了边(u,v),取出的这些边与其关联的点,组成一个点双连通分支。割点可以属于多个点双连通分支,其余点和每条边只属于且属于一个点双连通分支。对于边双连通分支,求法更为简单。只需在求出所有的桥以后,把桥边删除,原图变成了多个连通块,则每个连通块就是一个边双连通分支。桥不属于任何一个边双连通分支,其余的边和每个顶点都属于且只属于一个边双连通分支。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>                
    #include<map>
    using namespace std ;
    
    const int maxn = 5000 + 10 ;
    const int maxm = 20000 + 10 ;
    struct Edge{
        int to , next ;
        //该边是否为桥 
        bool cut ;
    }edge[maxm];
    int head[maxn] , tot ;
    
    //Low[u]表示u所能追溯到的在栈中最早的节点,该Low数组和求割点与桥的Low数组定义不一样 
    int Low[maxn] , DFN[maxn] , Stack[maxn] , Belong[maxn] ;
    int Index , top ;
    //边双连通块数 
    int block ;
    bool Instack[maxn] ;
    //桥的数目 
    int bridge ;
    
    void addedge( int u , int v){
        edge[tot].to = v ;edge[tot].next = head[u] ;edge[tot].cut = false ;
        head[u] = tot ++ ;
    }
    void Tarjan( int u , int pre){
        int  v ;
        Low[u] = DFN[u] = ++ Index ;
        Stack[top ++] = u ;
        Instack[u] = true ;
    
        for( int i = head[u] ; i != -1 ;i = edge[i].next ){
            v = edge[i].to ;
            if( v == pre) continue ;
            //树枝边 
            if( !DFN[v  ]){
                //回溯从叶子结点开始处理,一直到根节点 
                Tarjan( v , u ) ;
                //对于树枝边(u,v),修改low[u]之前一定有何v相连的回向边修改了 Low[v],造成v的Low数值变小 
                if( Low[u] > Low[v] )
                    Low[u] = Low[v] ;
                //桥边,回溯过后v所能追溯到的标号最小的节点依旧比u的编号大,这时Low[v] = DFN[v] ; 
                if( Low[v] > DFN[u]){
                    bridge ++ ;
                    edge[i].cut = true ;
                    edge[i ^ 1].cut = true ;
                } 
            }
            //(u,v)回向边,修改u所能追溯到的编号最小的节点 
            else if( Instack[v] && Low[u] > DFN[v])
                Low[u] = DFN[v] ;
        }
        //u节点是该连通分量的根节点 
        if( Low[u] == DFN[u]){
            block ++ ;
            do{
                v = Stack[ -- top ] ;
                Instack[v] = false ;
                Belong[v] = block ;
            }
            while( v != u ) ;
        }
    }
    
    void ini(){
        tot = 0 ;
        memset( head , -1 , sizeof( head ) ) ;
    } 
    
    //缩点后形成树,每个点的度数 
    int du[maxn] ;
    void solve( int n ){
        memset( DFN , 0 ,sizeof( DFN )) ;
        memset( Instack , false , sizeof( Instack )) ;
        Index = top = block = 0 ;
        Tarjan( 1 , 0 ) ;
    
    
        int ans = 0 ;
        memset( du , 0 , sizeof( du )) ;
        for(int i = 1 ;i<=n ;i++){
            for(int j = head[i] ;j!=-1 ; j=edge[j].next )
                if( edge[j].cut )
                    du[Belong [i]] ++;
        }
        for(int i = 1 ;i<=block ; i++)
            if( du [i] == 1)
                ans ++;
        printf("%d
    " , ( ans + 1) / 2 ) ;
    }
    
    int main(){
        int n , m ;
        int u , v ;
        while( scanf("%d%d" , & n , & m ) == 2){
            ini() ;
            while( m -- ){
                scanf("%d%d" , &u , & v ) ;
                addedge( u , v ) ;
                addedge( v , u ) ;
            }
            solve( n ) ;
        }
    }
    

    构造双连通图

    一个有桥的连通图,如何把它通过加边变成边双连通图?
    方法为首先求出所有的桥,然后删除这些桥边, 剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf。则至少在树上添加(leaf + 1) / 2条边,就能使树达到边二连通,所以至少添加的边数就是(leaf + 1) / 2。具体方法为,首先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf + 1) / 2 次,把所有点收缩到了一起。

  • 相关阅读:
    Leetcode题目practice
    文件操作
    39个奇葩代码注释,拿走不谢
    Spring Boot 之配置导入,强大到不行!
    Git 的这个神技,学会爽歪歪~
    同事天天写垃圾代码,就没办法?
    for (;;) 与 while (true),哪个更快?
    Spring Boot 怎么打一个可执行 Jar 包?
    程序员真的是太太太太太太太太难了!
    面试官:new一个对象有哪两个过程?
  • 原文地址:https://www.cnblogs.com/lunatic-talent/p/12798680.html
Copyright © 2011-2022 走看看