无向图的双连通分量

大佬的结论


无向图 
双连通分量
1 边的双连通分量  e-dcc  极大的不包含桥的连通块
2 点的双连通分量  v-dcc  极大的不包含割点的连通块

删除后不连通 的 两个定义

桥  
 o-o 桥o-o
 | | ↓ | |
 o-o - o-o 
1 如何找桥? x
            /
           y
x和y之间是桥 <=> dfn[x] < low[y] y无论如何往上走不到x //模板中反向边是不会遍历的
                +y能到的最高的点low[y] = dfn[y]

2 如何找所有边的双连通分量?
2.1 将所有桥删掉
2.2 stack
dfn[x] == low[x] 
<=> x无论如何走不到x的上面
<=> 从x开始的子树可以用一个栈存

每个割点至少属于两个连通分量

树里的每条边都是桥
     o
    / \
   o   o
  /\   /\
 o  o o  o

1 边双连通分量 
tarjan回顾
dfn[x] dfs序时间戳 
low[x] x能达到的时间戳最小的点

无向图不存在横插边
     o
    / \
   o   o
  /   / \
 y ← x   o
   →
x能往左的话 由于无向边 所以y也能往右,
那么在之前dfs的时候就把x先于x的父节点加进来了
*/
/*
新建一条道路 使得每两个草场之间都有一个分离的路径
给定一个无向连通图,问最少加几条边,可以将其变为一个边双连通分量

结论:一个边的双连通分量 <=> 任何两个点之间至少存在两个不相交路径
充分性: 对于每两个点都有互相分离的路径的话,则必然为强连通分量
反证 假设有桥(非双连通) x,y必然经过中间的桥 则只x→y的路径必在桥上相交
 o-x 桥y-o
 | | ↓ | |
 o-o - o-o
必要性:图是一个边双连通分量 <=> 不包含桥
        则一定对任意两点x,y 
        x,y之间至少存在两条互相分离(不相交)的路径
反证:假设存在两条相交路径
     那么x→y中间必然有桥
//  o-o-o-o-o 蓝色路径 (从x出发到y经过边数最少的路径)
//   - - - -  绿色路径 
//  x       y

对双连通分量做完缩点后 只剩桥和点
         o
        / \
       o   o
      /\   /\
     o  o o  o
    / \  
   o   o

         o
        / \
       o   o
      /\   /\
     o  o-o  o
    / \   |  |
   o   o__|  |
   |_________|
   可以发现对左右两个叶子节点连通后,根节点连向左右叶子节点的边就可以删去了
   同理 再把第2个和第4个叶子节点连通后,根节点连向第2个和第4个叶子节点的边也可以删去
   第3个叶子节点随便连
  给叶子节点按对称性加上边后就没有桥 <=> 变成边的双连通分量
  这里cnt= 5 加了ans=(cnt+1)/2=3条
  ans >= 下界[cnt/2]取整 == [(cnt+1)/2]取整
        其中 cnt为缩完点后度数==1的点的个数

作者:仅存老实人
链接:https://www.acwing.com/solution/content/20697/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1.求边的双连通分量

​​​

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 5010, M = 20010;

int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int id[N], dcc_cnt;
bool is_bridge[M];//每条边是不是桥
int d[N];//度数

void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++;
}

void tarjan(int u, int from)
{
    dfn[u] = low[u] = ++ timestamp;
    stk[ ++ top] = u;

    for (int i = h[u]; i!=-1; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])//j未遍历过,也可以防止反向边
        {
            tarjan(j, i);//dfs(j)
            low[u] = min(low[u], low[j]);//用j更新u
            if (dfn[u] < low[j])//j到不了u
                // 则x-y的边为桥,
                //正向边is_bridge[i] 反向边is_bridge[i ^ 1]都是桥
                is_bridge[i] = is_bridge[i ^ 1] = true;//i是idx,在建边时,0与1相连,2与3相连,构成正边和反边,只要正边是桥,那么反边也肯定是桥
                // 这里i==idx 如果idx==奇数 则反向边=idx-1 = idx^1
                //            如果idx==偶数 则反向边=idx+1 = idx^1
        }
        // j遍历过 且i不是反向边(即i不是指向u的父节点的边)
        // 因为我们不能用u的父节点的时间戳更新u
        else if (i != (from ^ 1))//如果不是反向边时
            low[u] = min(low[u], dfn[j]);
    }
    //双连通分量起点u  /
    //                u
    //               /
    //              ne1
    //             ne2 
    if (dfn[u] == low[u])
    {
        ++ dcc_cnt;
        int y;
        do {
            y = stk[top -- ];
            id[y] = dcc_cnt;
        } while (y != u);
    }
}

int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    while (m -- )
    {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);
    }

    tarjan(1, -1);//防止搜反向边 用一个from

    for (int i = 0; i < idx; i ++ )//要注意一定要遍历反向边,如果图是多点连接1个点时,每个点入度的值是1,不遍历反向边时入度是0
        //如果边i是桥 在其所连的出边的点j所在强连通分量的度+1
        // 桥两边的双连通分量各+1
        if (is_bridge[i])
            d[id[e[i]]] ++ ;

    int cnt = 0;
    for (int i = 1; i <= dcc_cnt; i ++ )
        if (d[i] == 1)//多少个度数为1的节点(强连通分量)
            cnt ++ ;//需要加的边的数量

    cout << (cnt + 1) / 2 << endl;

    return 0;
}

作者:仅存老实人
链接:https://www.acwing.com/solution/content/20697/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2.求点的双连通分量1 

3 割点
    割点 
 o-o   o-o
 |  \↓/  |
 o-o-o-o-o 
3.1求割点
     x   
     |
     y
要求low[y] > dfn[x] y能到达的最早的节点>x的时间戳
否则low[y] ≤ dfn[x]时,即y能到比xdfs序小的点,则存在环
    →o
   | ↓    
   | x   
   | |
   o←y
low[y]≤dfn[x]的情况下
1 如果x不是根节点 那么x是割点
2 x是根节点 至少有两个子节点yi 
            且有low[yi]≥dfn[x]
            如果只有一个子结点 去完根节点x
            x   
            |   →   y1  子节点部分还是连通的
            y1
            如果有两个子结点 去完根节点x
            x  
           / \  →   y1  y2 之间不连通
          y1 y2 
3.2 求点双连通分量
     o x
     |
     o y
stk[]
if(dfn(x)<=low(y))
{
    cnt++
    if(x非根||cnt>1)x是割点
    stk.pop(j) while j!=y将栈中元素弹出至y为止
    且x也属于该 点双连通分量
}
样例1
 0 - 1      0 - 1
  \ /     →        1个(不管删除哪个点都是连通的)
   2          
样例2
 0 - 1      0       
 2 - 3      2 - 3  2个(删除1后仍有0 和2-3两个连通块)
样例3
 0 - 1  删1还剩两个块 0 and 2
 2      删2还剩一个块 0-1
1 统计连通块个数cnt
2 枚举从哪个块j中删
  2.1 从块j中删除哪个点i
  2.2 删除点i后块j分成s部分(在样例3中删2后s=0)
      总共分成的部分 = s(i新的子块的个数)+cnt(删前总的连通块数)-1(删前子块的个数)
                     = 当前块的部分(s)+剩余连通块的数量(cnt-1)
                     = 1+1-1 (样例1:删2后 s=1 cnt=1 -1 =1
                     = 1+2-1 (样例2:删1后 s=1 cnt=2 -1 =2
                     = max(1+2-1,0+2-1)(样例3:删1后 s=1 cnt=2 -1=2 
                                              删2后 s=0 cnt=2 -1=1(点2所在的块删除2后没有点了 s=0)
                    dfn(x)<=low(y)
                    x删掉后y单独出来--多一个单独子树
                    如果x非根节点 还要多+1
                     /
                    x       
                   / \
    问题转化为依次删除每个割点
    求全局最大值


作者:仅存老实人
链接:https://www.acwing.com/solution/content/20702/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 10010, M = 30010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int root, ans;

void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++ ;
}

void tarjan(int u)
{
    dfn[u] = low[u] = ++ timestamp;
    int cnt = 0;//当前块内 已经可以分出来的子树的个数
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])//没有遍历过j
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);//用j更新u
            if (low[j] >= dfn[u]) cnt ++ ;//j为割点 则多一个连通块
        }
        else low[u] = min(low[u], dfn[j]);
    }
    if (u != root) cnt ++ ;//如果是一个环的话,可以发现此条件不成立,cnt=0;
    //如果不是根节点
    /*
             /
            x    删掉x后 除子节点yi外
           / \           还要要加上父节点部分+1
          o   o

    */
    ans = max(ans, cnt);
}
int main()
{
    while (cin >> n >> m, n || m)
    {
        memset(dfn, 0, sizeof dfn);
        memset(h, -1, sizeof h);
        idx = timestamp = 0;

        while (m -- )
        {
            int a, b;
            cin >> a >> b;
            add(a, b), add(b, a);
        }

        ans = 0;//把每个点删完后 最多能分为几块
        int cnt = 0;//连通块数量
        for (root = 0; root < n; root ++ )
            if (!dfn[root])//如果点root没搜索过 连通块数+1
            {
                cnt ++ ;
                tarjan(root);
            }

        cout << ans + cnt - 1 << endl;
    }

    return 0;
}


作者:仅存老实人
链接:https://www.acwing.com/solution/content/20702/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

3.求点的双连通分量2

 

 

给一个不一定连通的无向图
问最少在几个点设置出口
使得任意一个出口坏掉后,其他所有点都可以和某个出口连通

首先考虑
1 出口数量>=2 如果只有一个出口 那这个出口坏了就没有可以用的出口了
2 不同连通块之间相互独立(最终方案数 = 各连通块方案数乘积)
  对一个连通块
  2.1 无割点 <=> 不管我删掉哪个点 图剩余部分都是连通的、且剩余好出口>=1
             <=> 度数==0 
             <=> 孤立的点
      那么我们必须设置2个出口就可以满足
      连通块中点个数=cnt
      方案数=C[cnt][2] = cnt*(cnt-1)/2
  2.2 有割点 <=> 点双连通分量 缩点(每个割点都属于2个点双连通分量)
        2.2.1 先将所有割点单独出来作为一个点
        2.2.2 从每个点双连通分量(V-DCC)向其所包含的每个割点连一条边
     o       o
    / \     / \
   o---.---.---o 
        \ /
         o
    缩完点后 .-割点  o-连通块
    o-.-o-.-o  同时缩完后的o中包含之前有连的
        2.2.3 看V-DCC度数
            2.2.3.1 如果V-DCC==1 意味着它只包含一个割点(上图中左和右)
            则如果这个割点是出口且坏掉 这个V-DCC就无法连到其他出口了
            🔺所以V-DCC==1 时 必须在V-DCC内(非割点)放置一个出口
            2.2.3.2 如果V-DCC>1
            就不需要设置出口

证明:
1 如果割点坏了
    缩完点后,🔺此时所有度数==1的点相当于一个叶子节点-所有度数为1的叶子节点放一个出口
    因此每个叶子节点必然会有一个出口
        .(割点)
      ×/ \×   
      o   o(连通块)    
2 如果度数为1的右边的V-DCC里的某个点坏了
    由于是V-DCC 删去坏点后仍然是一个V-DCC
        .(割点)
       / \   
      o   @(连通块中坏了一个点)
    但此时不影响这个V-DCC到割点 并通过割点到左边V-DCC中的出口
3 如果度数为2的V-DCC里的某个点坏了
    由于度数==2 <=> 必然连接两个割点
        o(连通块)
       / \
      .   .(割点) 
    每个割点必然有一个出口
    所以可以从上面的o到下面的两个割点到其他V-DCC找到出口
总结:
1 无割点          放2个出口 方案数 *= C[cnt][2] = cnt*(cnt-1)/2
2 有割点 V-DCC==1 放1个出口 方案数 *= C[cnt-1][1] = cnt-1 (不包含割点)
3 有割点 V-DCC>=2 放0个出口 方案数 *= 1

作者:仅存老实人
链接:https://www.acwing.com/solution/content/24931/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

typedef unsigned long long ULL;

const int N = 1010, M = 1010;

int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int dcc_cnt;
vector<int> dcc[N];
bool cut[N];
int root;

void add(int a,int b)
{
    e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void tarjan(int u)
{
    low[u] = dfn[u] = ++timestamp;
    stk[++top] = u;
    // 1 u是孤立点-自称一个dcc
    if(u==root && h[u]==-1)//u是根节点且没有邻边
    {
        dcc_cnt++;
        dcc[dcc_cnt].push_back(u);
        return;
    }
    // 2 u不孤立
    int cnt = 0;
    for(int i = h[u];~i;i=ne[i])
    {
        int j = e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u],low[j]);
            // 看j是不是能连到比u还高的地方
            if(dfn[u]<=low[j])//j最高比u高度低 说明j是u一个新的分支(如果把u删掉 多一个j连通块)
            {
                cnt++;
                // 判断u是否割点 如果不是根节点-只要有一个分支他就是割点 || 如果是根节点 需要有两个分支才是割点
                //    root            /
                //    / \          非root(自带上面一个边,所以只要一个下分支)
                //                   /
                if(u!=root||cnt>1)cut[u] = true;
                ++dcc_cnt;
                int y;
                do{
                    y = stk[top--];
                    dcc[dcc_cnt].push_back(y);//存储的是这个割点所连的点(树中该点的子节点)
                }while(y!=j);//注意弹出栈不是弹到u为止 而是弹到j为止(u仍保留在stk中)
                // 🔺 开新分支 == u一定和新分支j组成一个dcc 也和旧连通块组成dcc
                // 那么当前最高点u还要被用在更高的包含u的旧连通块
                // 所以如果这个时候出栈了 回溯到比u高的点的时候 u就加不进旧连通块里
                dcc[dcc_cnt].push_back(u);
            }
        }
        else low[u] = min(low[u],dfn[j]);
    }
}

int main()
{
    int T = 1;
    while(cin >> m,m)
    {
        for(int i=1;i<=dcc_cnt;i++)dcc[i].clear();
        idx = n = timestamp = top = dcc_cnt = 0;
        memset(h,-1,sizeof h);
        memset(dfn,0,sizeof dfn);
        memset(cut,0,sizeof cut);
        while(m--)
        {
            int a,b;
            cin >> a >> b;
            n = max(n,b),n = max(n,a);//第二个n=漏了
            add(a,b),add(b,a);
        }

        for (root = 1; root <= n; root ++ )
            if (!dfn[root])
                tarjan(root);

        int res = 0;
        ULL num = 1;
        for(int i = 1;i<=dcc_cnt;i++)
        {
            int cnt = 0;
            for(int j= 0;j<dcc[i].size();j++)
            {
                if(cut[dcc[i][j]])//判断i的所连的点(包括自己)是否是割点
                    cnt++;
            }
            // 无割点
            if(cnt == 0)
            {
                if(dcc[i].size()>1)res+=2,num*=dcc[i].size()*(dcc[i].size()-1)/2;
                else res++;
            }
            else if(cnt==1)res++,num*=dcc[i].size()-1;
        }
        printf("Case %d: %d %llu\n", T ++, res, num);
    }
    return 0;
}

作者:仅存老实人
链接:https://www.acwing.com/solution/content/24931/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值