• 图的割点、桥与双连通分支


     

    本文转自:BYVoid 博客

    图的割点、桥与双连通分支

    [点连通度与边连通度]

    在一个无向连通图中,如果有一个顶点集合,删除这个顶点集合,以及这个集合中所有顶点相关联的边以后,原图变成多个连通块,就称这个点集为割点集合。一个图的点连通度的定义为,最小割点集合中的顶点数。

    类似的,如果有一个边集合,删除这个边集合以后,原图变成多个连通块,就称这个点集为割边集合。一个图的边连通度的定义为,最小割边集合中的边数。

    [双连通图、割点与桥]

    如果一个无向连通图的点连通度大于1,则称该图是点双连通的(point biconnected),简称双连通重连通。一个图有割点,当且仅当这个图的点连通度为1,则割点集合的唯一元素被称为割点(cut point),又叫关节点(articulation point)

    如果一个无向连通图的边连通度大于1,则称该图是边双连通的(edge biconnected),简称双连通或重连通。一个图有桥,当且仅当这个图的边连通度为1,则割边集合的唯一元素被称为桥(bridge),又叫关节边(articulation edge)。

    可以看出,点双连通与边双连通都可以简称为双连通,它们之间是有着某种联系的,下文中提到的双连通,均既可指点双连通,又可指边双连通。

    [双连通分支]

    在图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是割点,在无向连通图G中,
    1、根结点u为割顶当且仅当它有两个或者多个子结点;
    2、非根结点u为割顶当且仅当u存在结点v,使得v极其所有后代都没有反向边可以连回u的祖先(u不算)
    在Tarjan算法里面,有两个时间戳非常重要,一个是dfn,意为深度优先数,即代表访问顺序;一个是low,意为通过反向边能到达的最小dfn。于是,上述定理中第二个条件(非根结点)可以简单地写成low[v]>=dfn[u]。
    代码如下:

    int n,m,stamp,low[1005],dfn[1005],iscut[1005];
    vector<int> vec[1005];
    void tarjan(int index,int fa){
        int child=0;
        low[index]=dfn[index]=++stamp;
        for(int i=0;i<vec[index].size();i++)
        {
            int tmp=vec[index][i];
            if(!dfn[tmp])
            {
                child++;
                tarjan(tmp,index);
                low[index]=min(low[index],low[tmp]);
                if(low[tmp]>=dfn[index])
                    iscut[index]=1;
            }
            else if(dfn[tmp]<dfn[index] && tmp!=fa)
            {
                low[index]=min(low[index],dfn[tmp]);
            }
        }
        if(fa<0 && child==1)
            iscut[index]=0;
    }

    一条无向边(u,v)是桥,当且仅当(u,v)为树枝边,且满足DFS(u)<Low(v)。

    桥的求法其实也是类似的,它的求法可以看成是割顶的一种特殊情况,当结点u的子结点v的后代通过反向边只能连回v,那么删除这条边(u, v)就可以使得图G非连通了。用Tarjan算法里面的时间戳表示这个条件,就是low[v]>dfn[u]。
    代码如下:

    int n,stamp,dfn[1005],low[1005];
    int cnt,ansx[10005],ansy[10005];
    vector<int> vec[1005];
    int rank[1005];
    void addAns(int x,int y)
    {
        if(x>y)
            swap(x,y);
        ansx[cnt]=x, ansy[cnt]=y;
        cnt++;
    }
    void tarjan(int index,int fa)
    {
        int tmp;
        dfn[index]=low[index]=++stamp;
        for(int i=0;i<vec[index].size();i++)
        {
            tmp=vec[index][i];
            if(!dfn[tmp])
            {
                tarjan(tmp,index);
                low[index]=min(low[index],low[tmp]);
                if(low[tmp]>dfn[index])
                    addAns(index,tmp);
            }
            else if(dfn[tmp]<dfn[index] && tmp!=fa)
            {
                low[index]=min(low[index],dfn[tmp]);
            }
        }
    }

    [求双连通分支]

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

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

    对于边双连通分支,求法更为简单。只需在求出所有的桥以后,把桥边删除,原图变成了多个连通块,则每个连通块就是一个边双连通分支。桥不属于任何一个边双连通分支,其余的边和每个顶点都属于且只属于一个边双连通分支。

    另一种定义:
    对于一个连通图,如果任意两点至少存在两条点不重复路径,则称这个图为点双连通的(简称双连通);如果任意两点至少存在两条边不重复路径,则称该图为边双连通的。点双连通图的定义等价于任意两条边都同在一个简单环中,而边双连通图的定义等价于任意一条边至少在一个简单环中。对一个无向图,点双连通的极大子图称为点双连通分量(简称双连通分量),边双连通的极大子图称为边双连通分量。这篇博客就是总结一下求解无向图点双连通分量与边双连通分量的方法。

    算法
    求解点双连通分量与边双连通分量其实和求解割点与桥密切相关。不同双连通分量最多只有一个公共点,即某一个割顶,任意一个割顶都是至少两个点双连通的公共点。不同边双连通分量没有公共点,而桥不在任何一个边双连通分量中,点双连通分量一定是一个边双连通分量。
    下面首先介绍点双连通分量的Tarjan算法
    在之前的博客中,我们已经知道如何求解割顶了,很容易可以发现,当我们找到割顶的时候,就已经完成了一次对某个极大点双连通子图的访问,那么我们如果在进行DFS的过程中将遍历过的点保存起来,是不是就可以得到点双连通分量了?为了实现算法,我们可以在求解割顶的过程中用一个栈保存遍历过的(注意不是点!因为不同的双连通分量存在公共点即割顶),之后每当找到一个点双连通分量,即子结点v与父节点u满足关系low[v]>=dfn[u],我们就将栈里的东西拿出来直到遇到当前边。
    这里注意放入栈中的不是点,而是边,这是因为点双连通分量是存在重复点的,如果我们放入栈中的是点,那么对于某些点双连通分量,就会少掉一些点(这些点都是割顶)。
    代码:

    struct Edge{
        int u,v;
        Edge(int u=0,int v=0):u(u),v(v){}
    }e[maxm];
    int n,m,stamp,dfn[maxn],low[maxn],iscut[maxn],bccno[maxn];
    int scnt,stack[maxm],bcc_cnt;
    vector<int> vec[maxn],bcc[maxn];
    
    void tarjan(int index,int fa)
    {
        int child=0,tmp;
        dfn[index]=low[index]=++stamp;
        for(int i=0;i<vec[index].size();i++)
        {
            tmp=e[vec[index][i]].v;
            if(!dfn[tmp])
            {
                stack[++scnt]=vec[index][i],child++;
                tarjan(tmp,index);
                low[index]=min(low[index],low[tmp]);
                if(low[tmp]>=dfn[index])
                {
                    iscut[index]=1;
                    bcc[++bcc_cnt].clear();
                    while(1)
                    {
                        int num=stack[scnt--];
                        if(bccno[e[num].u]!=bcc_cnt)
                        {
                            bcc[bcc_cnt].push_back(e[num].u);
                            bccno[e[num].u]=bcc_cnt;
                        }
                        if(bccno[e[num].v]!=bcc_cnt)
                        {
                            bcc[bcc_cnt].push_back(e[num].v);
                            bccno[e[num].v]=bcc_cnt;
                        }
                        if(e[num].u==index && e[num].v==tmp)
                            break;
                    }
                }
            }
            else if(dfn[tmp]<dfn[index] && tmp!=fa)
            {
                stack[++scnt]=vec[index][i];
                low[index]=min(low[index], dfn[tmp]);
            }
        }
        if(fa<0 && child==1)
            iscut[index]=0;
    }
    
    void find_bcc()
    {
        // 割顶的bccno值无意义 
        memset(dfn,0,sizeof(dfn));
        memset(low,0,sizeof(low));
        memset(iscut,0,sizeof(iscut));
        memset(bccno,0,sizeof(bccno));
        memset(bcc,0,sizeof(bcc));
        stamp=scnt=bcc_cnt=0;
        for(int i=1;i<=n;i++)
            if(!dfn[i])
                tarjan(i,-1);
    }

    这里需要十分注意的是,算法结束之后,每个结点会有一个编号,代表它属于哪一个点双连通分量,但是,割顶的编号是完全没有意义的!这个算法灵活使用了两个时间戳和栈,完成了点双连通分量的发现。
    例题:UVALIVE 5135

    之后介绍边双连通分量的求解算法:
    边双连通分量的求解非常简单,因为边双连通分量之间没有公共边,而且桥不在任意一个边双连通分量中,所以算法十分简单,即先一次DFS找到所有桥,再一次DFS(排除了桥)找到边双连通分量。
    PS:当然可以用一次DFS实现。
    代码:

    struct Edge{
        int u,v;
        Edge(int u=0,int v=0):u(u),v(v){}
    }e[maxm];
    int n,m,stamp,dfn[maxn],low[maxn],bccno[maxn],bcc_cnt;
    vector<int> vec[maxn],bcc[maxn];
    bool g[maxn][maxn],isbridge[maxm];
    
    void tarjan(int index,int fa)
    {
        int tmp;
        dfn[index]=low[index]=++stamp;
        for(int i=0;i<vec[index].size();i++)
        {
            tmp=e[vec[index][i]].v;
            if(!dfn[tmp])
            {
                tarjan(tmp,index);
                low[index]=min(low[index],low[tmp]);
                if(low[tmp]>dfn[index])
                    isbridge[vec[index][i]]=isbridge[vec[index][i]^1]=1;
            }
            else if(dfn[tmp]<dfn[index] && tmp!=fa)
            {
                low[index]=min(low[index], dfn[tmp]);
            }
        }
    }
    
    void dfs(int index)
    {
        dfn[index]=1;
        bccno[index]=bcc_cnt;
        for(int i=0;i<vec[index].size();i++)
        {
            int tmp=vec[index][i];
            if(isbridge[tmp])
                continue;
            if(!dfn[e[tmp].v])
            {
                dfs(e[tmp].v);
            }
        }
    }
    
    void find_ebcc(){
        bcc_cnt=stamp=0;
        memset(dfn,0,sizeof(dfn));
        memset(low,0,sizeof(low));
        memset(isbridge,0,sizeof(isbridge));
        memset(bccno,0,sizeof(bccno));
        memset(bcc,0,sizeof(bcc));
        for(int i=1;i<=n;i++)
            if(!dfn[i])
                tarjan(i, -1);
        memset(dfn,0,sizeof(dfn));
        for(int i=1;i<=n;i++)
        {
            if(!dfn[i])
            {
                bcc_cnt++;
                dfs(i);
            }
        }               
    }
    POJ 3352

    [构造双连通图]

    一个有桥的连通图,如何把它通过加边变成边双连通图?方法为首先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。

    统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf。则至少在树上添加(leaf+1)/2条边,就能使树达到边二连通,所以至少添加的边数就是(leaf+1)/2。具体方法为,首先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf+1)/2次,把所有点收缩到了一起。

    [图的双连通性问题例题]

    备用交换机 求图的割点,直接输出。

    pku 3177(3352) Redundant Paths 求桥,收缩边双连通子图,构造边双连通图。

    POI 1999 仓库管理员 Store-keeper 求点双连通子图。

  • 相关阅读:
    揭开Socket编程的面纱(留着自己慢慢看)
    XML 新手入门基础知识
    RocketMQ集群平滑下线或重启某个节点
    RocketMQ borker配置文件
    ES:在线迁移集群索引,数据不丢失
    SQL命令汇总
    Redis过期key淘汰策略
    中间件服务器内核参数优化
    在线做RAID命令
    CPU网卡亲和绑定
  • 原文地址:https://www.cnblogs.com/mhpp/p/6751716.html
Copyright © 2020-2023  润新知