【图论】无向图的双连通分量

算法提高课笔记

理论基础

  • 边的双连通分量 e-DCC:极大不包含桥的连通块
  • 点的双连通分量 v-DCC:极大不包含割点的连通块

:删去该边图就不连通
割点:删去该点和关联边图就不连通

每个割点至少属于两个连通分量
两个割点之间不一定是桥,连接两个桥的点也不一定是割点

边的双连通分量问题

引入时间戳概念

记录dfn(x):遍历到每个点的时间
low(x):每个连通分量的最高点

如何找到桥?

图中一条边[x, y],如果y可以走到更上面,就不是桥,如果y最多只能走到自己,那就是桥

在这里插入图片描述
因此,一条边是桥等价于dfn[y] == low[y]

如何找到所有边的双连通分量?

方法1. 将所有桥删掉

方法2. 用一个栈存,如果找到一个x使得dfn[x] == low[x],那么x的子树中还在栈中的结点就是x的连通分量的结点

例题:冗余路径

原题链接

为了从 F 个草场中的一个走到另一个,奶牛们有时不得不路过一些她们讨厌的可怕的树。

奶牛们已经厌倦了被迫走某一条路,所以她们想建一些新路,使每一对草场之间都会至少有两条相互分离的路径,这样她们就有多一些选择。

每对草场之间已经有至少一条路径。

给出所有 R 条双向路的描述,每条路连接了两个不同的草场,请计算最少的新建道路的数量,路径由若干道路首尾相连而成。

两条路径相互分离,是指两条路径没有一条重合的道路。

但是,两条分离的路径上可以有一些相同的草场。

对于同一对草场之间,可能已经有两条不同的道路,你也可以在它们之间再建一条道路,作为另一条不同的道路。

输入格式

第 1 行输入 F 和 R。

接下来 R 行,每行输入两个整数,表示两个草场,它们之间有一条道路。

输出格式

输出一个整数,表示最少的需要新建的道路数。

数据范围

1 ≤ F ≤ 5000 , 1≤F≤5000, 1F5000,
F − 1 ≤ R ≤ 10000 F−1≤R≤10000 F1R10000

输入样例

7 7
1 2
2 3
3 4
2 5
4 5
5 6
5 7

输出样例

2
题意

给出无向连通图,求问至少加多少条路径使得成为边的双连通分量

思路

边的双连通分量意味着不含桥(不具体证明了)

找到所有边的双连通分量之后,进行缩点,之后图就会变成一棵树

树中所有度数为1的点都需要加一条边(否则把原有边砍掉之后就不连通了)

所以需要加的边数至少是 cnt/2 上取整(叶子结点的个数)

像这样连,发现取cnt/2上取整是完全满足条件的 于是答案是 (cnt+1)/2

代码
#include <bits/stdc++.h>

using namespace std;

const int N = 5010, M = 20010;

int n, m;
int h[N], ne[M], e[M], idx;
int dfn[N], low[N], timestamp;
stack<int> stk;
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) // from是父结点
{
    dfn[u] = low[u] = ++ timestamp; // 先将dfn和low都初始化为时间戳
    stk.push(u); // u加入栈中

    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i]; // 取出u的所有邻点j
        if (!dfn[j]) // 如果j还没被遍历
        {
            tarjan(j, i);
            low[u] = min(low[u], low[j]); // 用low[j]更新low[u]
            if (dfn[u] < low[j]) // 说明j到不了u 即当前这条边是一个桥
                is_bridge[i] = is_bridge[i ^ 1] = true; // 正向边和反向边
        }
        else if (i != (from ^ 1)) // 不是反向边
            low[u] = min(low[u], dfn[j]); // 更新low[u]
    }

    if (dfn[u] == low[u]) // 如果该点是所在强连通分量的最高点
    {
        ++ dcc_cnt; // 更新连通块数量
        int y;
        do
        {
            y = stk.top(); // 取出栈顶元素
            stk.pop();
            id[y] = dcc_cnt; // 标记每个点所在的连通分量编号
        } while (y != u); // 直到取到此连通分量的最高点为止
    }
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    memset(h, -1, sizeof h);

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

    tarjan(1, -1);

    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 ++ ;

    cout << (cnt + 1) / 2 << '\n'; 
}

点的双连通分量问题

同样需要记录时间戳

dfn(x):遍历到每个点的时间
low(x):每个连通分量的最高点

如何判断割点?

我们从当前开始搜,也就是从x搜到y,判断x是否为割点只要看y是否能搜到x上面
在这里插入图片描述
如果y能搜到x上方,说明x一定不是割点

所以要求 low(y) >= dfn(x)

  • 如果x不是根结点,那么x必然是割点
  • 如果x是根结点,还要分两种情况讨论:
    • 如果x只有一个子树,那么删去x对原来的连通性没有影响,x不是割点
    • 如果x有两个及以上的子树,那么删去x会影响图的连通性,x是割点。该条件可以转换成low(y) >= dfn(x)
      在这里插入图片描述

如何求点的双连通分量?

例题:电力

原题链接

给定一个由 n 个点 m 条边构成的无向图,请你求出该图删除一个点之后,连通块最多有多少。

输入格式

输入包含多组数据。

每组数据第一行包含两个整数 n,m。

接下来 m 行,每行包含两个整数 a,b,表示 a,b 两点之间有边连接。

数据保证无重边。

点的编号从 0 到 n−1。

读入以一行 0 0 结束。

输出格式

每组数据输出一个结果,占一行,表示连通块的最大数量。

数据范围

1 ≤ n ≤ 10000 , 1≤n≤10000, 1n10000,
0 ≤ m ≤ 15000 , 0≤m≤15000, 0m15000,
0 ≤ a , b < n 0≤a,b<n 0a,b<n

输入样例

3 3
0 1
0 2
2 1
4 2
0 1
2 3
3 1
1 0
0 0

输出样例

1
2
2
题意

给出一张图,删掉一个结点,问连通块最多有多少

思路
  1. 首先统计连通块个数cnt
  2. 枚举哪个连通块中删,确定了连通块之后再枚举删除哪个点,分别求一下删除该店之后可以把当前连通块分成几部分
  3. 假设可以分为s部分,那么总共连通块个数就是s + cnt - 1

现在问题就变成了s怎么算呢?

我们要在这个地方删去x点,如果x不是根结点,那么删完x有三部分,就像这样:
在这里插入图片描述如果x是根结点,那么删完x由两部分,就像这样:
在这里插入图片描述/.;

代码
#include <bits/stdc++.h>

using namespace std;

const int N = 10010, M = 30010;

int n, m;
int h[N], ne[M], e[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; // 先将dfn和low都初始化为时间戳

    int cnt = 0; // 记录删去当前点后当前连通块能变成多少个连通块
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i]; // 取出u的所有邻点j
        if (!dfn[j]) // 如果j还没被遍历
        {
            tarjan(j);
            low[u] = min(low[u], low[j]); // 用low[j]更新low[u]
            if (low[j] >= dfn[u]) // 说明j到不了u以上的点 即删去u后连通块数量加1
                cnt ++ ;
        }
        else low[u] = min(low[u], dfn[j]); // 更新low[u]
    }

    if (u != root) cnt ++ ; // u不是根结点 删去u原连通块分成三部分(也就是在原来两部分的基础上再加1)

    ans = max(ans, cnt);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    while (cin >> n >> m, n || m)
    {
        memset(h, -1, sizeof h);
        memset(dfn, 0, sizeof dfn);
        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])
            {
                cnt ++ ; // 总连通块数量加1
                tarjan(root); // 遍历当前连通块
            }

        cout << ans + cnt - 1 << '\n';
    }
}
  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Texcavator

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值