无向图的双连通分量

无向图的双连通分量

1. 无向图的双连通分量原理

原理

  • 双联通分量也被称为重连通分量,一下内容都是针对无向图来说的。
  • 无向图的双联通分量可以分为两类:

(1)边双联通分量(e-DCC​);

(2)点双联通分量(v-DCC);

  • 对于(1)边双联通分量的定义,我们需要引入的概念,桥是指连通图中的一条边,这条边满足:如果删除这条边,整个图会变的不连通,则这条边被称为桥。极大的不含有桥的连通区域称为边连通分量。根据定义可知e-DCC有以下性质:

    ① 在e-DCC中,无论删除哪条边,该e-DCC仍是连通的;

    ② 在e-DCC中,任意两点之间至少存在两条不相交(边是严格不相交的,点可以相交)的路径;

在这里插入图片描述

  • 对于(2)点双联通分量的定义,我们需要引入割点的概念,割点是指连通图中的一个点,这个点满足:如果删除这个点以及该点相关联的所有边,整个图会变的不连通,则这个点被称为割点。极大的不含有割点的连通区域称为点连通分量。根据定义可知v-DCC有以下性质:

    ① 每个割点至少属于两个v-DCC

在这里插入图片描述


  • 割点和桥之间没有任何关系

(1)两个割点之间的边一定是桥吗?不一定是,如下图:

在这里插入图片描述

(2)一个桥的两个端点一定是割点吗?不一定是,如下图:

在这里插入图片描述

  • 边连通分量和点连通分量之间也没有任何关系

(1)一个图是边连通分量,则它一定是点连通分量吗?不一定是,如下图:

在这里插入图片描述

(2)一个图是点连通分量,则它一定是边连通分量吗?不一定是,如下图:

在这里插入图片描述

  • 对于一棵树来说,所有的边都是桥,因此树中的每个点都是一个边连通分量;除叶节点外的所有节点都是割点,因此每条边以及边的两个端点构成的图都是点连通分量。

边双联通分量(e-DCC)

  • 如何求解边双联通分量?这里的做法和求有向图的强联通分量(网址)是类似的,也需要时间戳以及 d f n 、 l o w dfn、low dfnlow数组。

  • 引入时间戳(从1开始计数)的概念,根据DFS过程中每个节点遍历到的顺序依次给每个节点递增赋值。

  • 对于每个节点,定义两个时间戳: d f n [ u ] dfn[u] dfn[u] l o w [ u ] low[u] low[u]

    d f n [ u ] dfn[u] dfn[u]:表示遍历到u的时间戳;

    low[u]:从u开始走,所能遍历到的最小时间戳。

  • 如何找到所有的桥?边(x, y)是桥    ⟺    d f n [ x ] < l o w [ y ] \iff dfn[x] < low[y] dfn[x]<low[y]

  • 如何找到所有的边连通分量?存在两种做法:

(1)将所有的桥删除,剩余的每个连通分量就是e-DCC。

(2)类似于有向图求SCC,可以使用一个栈记录当前边连通分量中的点,如果有 d f n [ u ] = = l o w [ u ] dfn[u] == low[u] dfn[u]==low[u],说明此时递归过程中走到x的边是桥,此时可以弹出栈中的元素,得到一个e-DCC。

点双联通分量(v-DCC)

  • 此时仍然需要使用时间戳以及 d f n 、 l o w dfn、low dfnlow数组。

  • 首先我们需要考虑如何求割点?考虑DFS过程中从点u遍历到点j,如果有 d f n [ u ] ≤ l o w [ j ] dfn[u] \le low[j] dfn[u]low[j],则:

    ① 如果x不是根节点,那么x是割点;

    ② 如果x是根节点,则至少存在两各个节点y,使得 l o w [ y i ] ≥ d f n [ x ] low[y_i] \ge dfn[x] low[yi]dfn[x],此时x才是割点。

  • 如何求点连通分量(v-DCC)呢?

    首先如果一个点是一个孤立点的话,也是一个v-DCC;

    做法类似于有向图求SCC,可以使用一个栈记录当前边连通分量中的点,如果在DFS过程中从点u遍历到点j,有 d f n [ u ] ≤ l o w [ j ] dfn[u] \le low[j] dfn[u]low[j],说明u可能是割点,具体步骤如下:

    if (dfn[u] <= low[j]) {
    	cnt++;  // cnt记录u的子树的个数
    	if (x非根 || cnt > 1) x是割点;
    	将栈中元素弹出直至弹出y为止;
    	u也属于该v-DCC;
    }
    
  • 我们可能对下图存在疑问,下图中j是割点,u不是v-DCC中的点?

在这里插入图片描述

  • 上图中其实u也是v-DCC中的点,但是上图有两个v-DCC,如下图:

在这里插入图片描述

2. AcWing上的无向图的双连通分量题目

AcWing 395. 冗余路径

问题描述

分析

  • 在一个连通图中,任意两点之间至少存在两条不相交(边是严格不相交的,点可以相交)的路径    ⟺    \iff 这个图是一个边双联通分量。这里不给证明,记住结论即可。
  • 根据上面的结论,则这个题目相当于问:给我们一个无向连通图,问至少加几条边,可以将其变为一个边双联通分量(e-DCC)。注意可以认为AcWing 367. 学校网络是这一题的有向图版本,问的是有向图中加几条边能够成为SCC。
  • 具体的做法是:将图中所有的边连通分量进行缩点,缩点完成后的新图就会变成一棵树,对于树中所有的叶子节点(假设有cnt个),我们都至少要给他们加上一条边,因此我们至少需要给图中添加 ⌈ c n t / 2 ⌉ = ⌊ ( c n t + 1 ) / 2 ⌋ \lceil cnt/2 \rceil = \lfloor (cnt+1)/2 \rfloor cnt/2=(cnt+1)/2条边。

  • 因此,最终的结论是:

(1)在一个无向连通图中,如果该图原本就是一个e-DCC,则不需要加边就满足条件;对图中所有e-DCC缩点之后图会变成一棵树,若新图中叶节点个数为cnt,则图中添加 ⌈ c n t / 2 ⌉ = ⌊ ( c n t + 1 ) / 2 ⌋ \lceil cnt/2 \rceil = \lfloor (cnt+1)/2 \rfloor cnt/2=(cnt+1)/2条边即可让整张图变为一个e-DCC。对应本题。

(2)在一个有向图中,如果该图原本就是一个SCC,则不需要加边就满足条件;否则对图中所有的SCC进行缩点之后,若起点有P个,终点有Q个,则至少增加 M A X ( P , Q ) MAX(P, Q) MAX(P,Q)条边即可让整个有向图成为一个SCC。对应AcWing 367. 学校网络

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 5010, M = 20010;

int n, m;
int h[N], e[M], ne[M], idx;

int dfn[N];  // dfn[u]: 表示遍历到u的时间戳
int low[N];  // low[n]: 从u开始走,所能遍历到的最小时间戳。
int timestamp;  // 时间戳

int stk[N], top;  // 存储在当前e-DCC中的点

int id[N];  // 表示某个点所在的e-DCC编号
int dcc_cnt;  // 表示当前有多少个e-DCC

bool is_bridge[M];  // 记录每条边是否是桥

int d[N];  // 每个e-DCC的度数

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

// 点u是从边from过来的,防止从u向边from回搜
void tarjan(int u, int from) {
    
    dfn[u] = low[u] = ++ timestamp;
    stk[++top] = u;
    
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (!dfn[j]) {
            tarjan(j, i);
            low[u] = min(low[u], low[j]);
            if (dfn[u] < low[j])  // 说明从j无法回到前面,(u, j)是桥
                is_bridge[i] = is_bridge[i ^ 1] = true;
        } else if (i != (from ^ 1))  // 第i条边不能是回去的边
            low[u] = min(low[u], dfn[j]);
    }
    
    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;
        // 第0条边和第1条边是一对,之后类似
        // 如果知道其中一条边的是第i条边,则另一条边是第i^1条边
        add(a, b), add(b, a);
    }
    
    tarjan(1, -1);
    
    // idx记录的边的数目
    // 统计每个e-DCC的出度
    for (int i = 0; i < idx; i++)
        if (is_bridge[i])
            d[id[e[i]]]++;
    
    // 统计叶子节点的数目
    int cnt = 0;
    for (int i = 1; i <= dcc_cnt; i++)
        if (d[i] == 1)
            cnt++;
    
    printf("%d\n", (cnt + 1) / 2);
    
    return 0;
}

AcWing 1183. 电力

问题描述

分析

  • 做法如下:

(1)统计一下所有连通块的个数(结果为cnt);

(2)枚举从哪个块中删,然后再枚举这个块中删除哪个点,会得到删除点之后形成块的数量,去最大值,记为ans。最终的答案就是ans+cnt-1。

  • 我们需要考虑删除哪个点?答案是我们应该删除每个连通块中的割点,因为根据割点定义,只有删除割点才能使连通块的数量增加。对于每个连通块而言,如果u是割点,假设u存在2个孩子,此时还需要判断u是否为这个图的根节点(即遍历该连通块时第一个遍历到的节点),如果是根节点,则删去u之后能形成2个连通块,如果u不是根节点,则删去u之后能形成3个连通块,如下图:

在这里插入图片描述

代码

  • C++
#include <iostream>
#include <cstring>

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;  // 记录每个连通块的"根节点"
int 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 s = 0;  // 如果当前点u是割点的话,去掉该点u得到的连通分量的个数
    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]);
            if (dfn[u] <= low[j])  // 说明u是可能是割点, u存在一棵子树
                s++;
        } else low[u] = min(low[u], dfn[j]);
    }
    
    //如果不是根节点
    /*
             /
            u    删掉u后 除子节点yi外
           / \           还要要加上父节点部分+1
          o   o
    */
    if (u != root) s++;  // 不用加上&& s的判断,因为u不是割点的话,s要取1
    
    ans = max(ans, s);
}

int main() {
    
    while (scanf("%d%d", &n, &m), n || m) {
        
        memset(dfn, 0, sizeof dfn);  // dfn还具有判重数组的作用
        memset(h, -1, sizeof h);
        idx = timestamp = 0;
        
        while (m--) {
            int a, b;
            scanf("%d%d", &a, &b);
            add(a, b), add(b, a);
        }
        
        ans = 0;
        int cnt = 0;  // 记录连通块的数目
        for (root = 0; root < n; root++)  // 节点编号从0~n-1
            if (!dfn[root]) {
                cnt++;
                tarjan(root);
            }
        
        printf("%d\n", cnt + ans - 1);
    }
    
    return 0;
}

AcWing 396. 矿场搭建

问题描述

分析

  • 本题相当于问:给定一个无向图,问最少在几个点上设置出口,可以使得不管其他哪个点被删除,其余所有点都可以与某个出口连通。
  • 本题中给的无向图可能不是连通的,存在多个连通分量,我们不需要考虑连通分量的情况,我们考虑每个v-DCC即可。如果某个连通分量中不含有割点,则该连通分量就是一个v-DCC,对应下面的情况(1)。
  • 假设每个v-DCC的需要设置的通道数记为 r e s [ i ] res[i] res[i],方案数为 n u m [ i ] num[i] num[i],则最终整个图需要的通道数为 r e s = ∑ r e s [ i ] res = \sum res[i] res=res[i],方案数为 n u m = ∏ n u m [ i ] num = \prod num[i] num=num[i]。首先,我们的出口数量必须有 r e s ≥ 2 res \ge 2 res2,否则只存在一个出口的话,万一出口坏了,则其他点都出不去了。下面我们聚焦讨论每个v-DCC(假设当前讨论的第i个v-DCC点的数量为size个,割点数量为cnt个,这可以使用tarjan算法求解)。

(1)如果v-DCC中不含有割点(这种情况对应图中某个连通分量中不存在割点),即cnt==0,则需要设置两个出口( r e s [ i ] = 2 res[i]=2 res[i]=2),这两个出口可以任意选两个,方案数 n u m [ i ] = C s i z e 2 num[i]=C_{size}^2 num[i]=Csize2

(2)如果v-DCC中有割点,即cnt>1,则需要对该v-DCC所在的连通分量进行缩点操作(实际代码中不需要进行缩点,只需要计算cnt即可,这里为了分析方便),这里的缩点规则是:每个割点单独作为一个点;从每个v-DCC向其所包含的每个割点连边。如下图(缩点后至少存在三个点,我们不需要考虑单独的割点,考虑割点所在的v-DCC即可):

在这里插入图片描述

缩点之后边的个数不会增加,但是点的数量可能增加,新图中点的个数=连通分量的个数+割点的数目,因此最多有2倍的点。

如上图,因为这个连通分量存在割点,因此存在v-DCC,具体来说,上图对应的连通分量3个v-DCC,我们需要依次考虑每个v-DCC。

(2.1)如果cnt==1,如上图中的绿色和青色对应的缩点,这个割点相当于出口,则我们需要在该v-DCC中出除了割点的位置外设置一个出口即可,方案数为size-1;这样能保证该v-DCC的安全,因为如果割点坏了,通过该v-DCC中的出口该v-DCC中的其他点可以出去;如果这个出口坏了,可以通过这个割点到达其他v-DCC,也可以通过其他v-DCC设置的出口安全出去。

(2.2)如果cnt>=2,如上图中的紫色对应的缩点,则该v-DCC不需要设置任何出口;因为无论该v-DCC中的哪个点坏了,都可以通过割点到达其他v-DCC,然后通过其他v-DCC设置的出口安全出去。

  • 最后我们还需要考虑孤立点的情况,孤立点不是割点,我们在孤立点也需要设置一个出口,否则若其他点坏了,这各孤立点没法通过出口出去。
  • 另外这一题没有给点数,但是是从1开始的自然数,我们需要自己求一下点数。

代码

  • C++
#include <iostream>
#include <cstring>
#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;  // v-DCC的个数
vector<int> dcc[N];  // 存储每个v-DCC有哪些点,之后用来求每个v-DCC中割点的数量
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) {
    
    dfn[u] = low[u] = ++timestamp;
    stk[++top] = u;
    
    // 判断点u是否为孤立点
    if (u == root && h[u] == -1) {
        dcc[++dcc_cnt].push_back(u);
        return;
    }
    
    int cnt = 0;  // u的不能回到u之前的子树数量
    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]);
            if (dfn[u] <= low[j]) {
                cnt++;
                /* 判断割点对应两种情况:
                         /
                        u
                       / \
                      o   o
                */
                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);
                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, a), n = max(n, b);
            add(a, b), add(b, a);
        }
        
        // 求v-DCC
        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;  // 该v-DCC对应的割点数目
            int t = dcc[i].size();  // 当前v-DCC中点的数量
            for (int j = 0; j < t; j++)
                if (cut[dcc[i][j]])
                    cnt++;
            
            if (cnt == 0) {
                if (t == 1) res++;
                else res += 2, num *= t * (t - 1) / 2;
            } else if (cnt == 1)
                res++, num *= t - 1;
        }
        
        printf("Case %d: %d %llu\n", T++, res, num);
    }
    
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值