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

[点连通度与边连通度]

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

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

[割点]


一个图有割点,当且仅当这个图的点连通度为1,则割点集合的唯一元素被称为割点(cut point),又叫关节点(articulation point)切除该点图的连通分量数+1.也就是说原有的一个连通分量经过操作成为两个连通分量,原图不再连通。

(如果有多个割点,则每个割点可以自成一格最小割点集合)

[桥]


一个图有桥,当且仅当这个图的边连通度为1,则割边集合的唯一元素被称为桥(bridge),又叫关节边(articulation edge)。删掉该边图的连通分量数+1,原图不再连通。

[双连通图、割点与桥]

如果一个无向连通图的点连通度大于1,则称该图是点双连通的(point biconnected),简称双连通重连通。简单来说就是删除任何一结点,不影响图的连通性。图没有割点。

如果一个无向连通图的边连通度大于1,则称该图是边双连通的(edge biconnected),简称双连通或重连通。简单来说就是删除任何一边,不影响图的连通性。图没有桥。

点双连通与边双连通都可以简称为双连通,它们之间是有着某种联系的。

[双连通分支]

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


求解方法

由dfs构造的一棵搜索树中:

1.割点:点u为割点,满足以下两个条件之一

1>.u为树的root && u的孩子≥2个

2>.u不为树的root && 满足DFN[u]<=LOW[v];

其中DFN为dfs过程中节点的编号。LOW为该点引出的边中最多直接或间接连接的最早的祖先。

1>.条件很形象.

2>.当出现DFN[u]<=LOW[v]时,也就是说v点最多达到u,不可能再向上达到u的祖先节点。显然这个时候将u点删除,v所在的一团和u点祖先的一团分割为两个连通分量。所以u为割点。

2.桥:边(u,v)为桥,满足该条件:DFN[u]<LOW[v]。这里照上面的讲解也很形象。当DFN[u]==LOW[v]时,当u->v dfs递归,存在一条v->u的回边,使得LOW[v]=DFN[u];故不为桥。

割和桥的两种判定就像上面的讲述:


void Tarjan( int u,int father )  
{  
     Node *p=ptr[u];  
     DFN[u]=LOW[v]=++DEP;  
     while( p )  
     {  
            if( DFN[p->v]==0 )  
            {  
                Tarjan( p->v,u );  
                LOW[u]=min( LOW[u],LOW[v] );  
                //if DFN[u]<LOW[v] 则(u,v)为桥;   
            }  
            else if( p->v!=father )  
                 LOW[u]=min( LOW[u],DFN[v] );  
            p=p->next;  
     }  
     /* 
     if( u is root ) 
         u是割点 <=> u至少有两个孩子 
     else 
         u是割点 <=> DFN[u]<=LOW[v]  
     */   
}

void getV_B()  
{  
     Tarjan(1,0);  
     int rootSon=0,cutPoint=0;  
     for( i=2;i<=N;i++ )  
     {  
          if( father[i]==1 )  
              rootSon++;  
          else if( DFN[father[i]<=LOW[i]] )  
              cutPoint++;//i为割点  
     }  
     if( rootSon>=2 )  
         cntPoint++;//root点1为割点  
     for( i=1;i<=N;i++ )  
          if( DFN[father[i]]<LOW[i] )  
              //( father[i],i )为桥;   
}  

3.边双连通分支:

也就是将桥删除后整个图分成的连通块就是边双连通分支。

4.点双连通分支:

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

void Tarjan(int u, int 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])   
        { //v没有访问过//树边要入栈  
            Edges.push_back(Edge2(u,v));  
            Tarjan(v,u);  
            low[u] = min(low[u],low[v]);  
            Edge2 tmp(0,0);  
            if(dfn[u] <= low[v])   
            { //从一条边往下走,走完后发现自己是割点,则栈中的边一定全是和自己在一个双连通分量里面//根节点总是和其下的某些点在同一个双连通分量里面  
                  cout << "Block No: " << ++ nBlockNo << endl;  
                  do   
                  {  
                     tmp = Edges.back();  
                     Edges.pop_back ();  
                     cout << tmp.u << "," <<tmp.v << endl;  
                   }while ( !(tmp.u == u && tmp.v == v) );  
            }  
        }  // 对应if( ! dfn[v]) {  
        else if( v != father )  
        {  
             //u连到父节点的回边不考虑  
             low[u] = min(low[u],dfn[v]);  
             if( dfn[u] > dfn[v])//子孙连接到祖先的回边要入栈,但是子孙连接到自己的边,此处肯定已经入过栈了,不能再入栈  
                 Edges.push_back(Edge2(u,v));  
        }  
    } //对应  for( i = 0;i < G[u].size() ;i ++ ) {  
}


5.构造双连通图

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

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

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <stack>
using namespace std;

const int MAXN = 5011;
const int MAXM = 20011;

int now[MAXN], dfn[MAXN], low[MAXN], d[MAXN], color[MAXN];
int pre[MAXM], son[MAXM], vis[MAXM];

stack<int> st;

int n, edgeCnt, dfnCnt, ebccCnt;

void addEdge(int u, int v)
{
    vis[edgeCnt] = 0;
    pre[edgeCnt] = now[u]; son[edgeCnt] = v; now[u] = edgeCnt ++;
}

void dfs(int u, int fa)
{
    dfn[u] = low[u] = ++ dfnCnt;
    st.push(u);
    for (int p = now[u]; p != -1; p = pre[p])
    {
        if (vis[p]) continue;
        vis[p] = vis[p ^ 1] = true;

        int v = son[p];
        if (!dfn[v])
        {
            dfs(v, u);
            low[u] = min(low[u], low[v]);
        }
        else
            low[u] = min(low[u], dfn[v]);

    }

    if (dfn[u] == low[u])
    {
        ++ ebccCnt;
        while (st.top() != u)
        {
            color[st.top()] = ebccCnt;
            st.pop();
        }
        color[u] = ebccCnt;
        st.pop();
    }
}

void init()
{
    edgeCnt = dfnCnt = ebccCnt = 0;
    memset(dfn, 0, sizeof(dfn));
    memset(now, 255, sizeof(now));
    memset(color, 255, sizeof(color));

    int m;
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; ++ i)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        addEdge(u, v);
        addEdge(v, u);
    }
}

void solve()
{
    dfs(1, 0);

    memset(d, 0, sizeof(d));
    for (int i = 1; i <= n; ++ i)
    {
        for (int p = now[i]; p != -1; p = pre[p])
            if (color[i] != color[son[p]])
                d[color[i]] ++;
    }

    int leafCnt = 0;
    for (int i = 1; i <= ebccCnt; ++ i)
        if (d[i] == 1)
            leafCnt ++;
    printf("%d\n", (leafCnt + 1) / 2);
}

int main()
{
    init();
    solve();
    return 0;
}












评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值