无向图的双连通分量

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

每个割点至少属于两个连通分量
任何两个割点之间的边不一定是桥
任何一条桥的两个端点不一定是割点

割点:如果把该点删除后连通块的个数增加,那么该点为割点
桥:如果把该边删除后连通块的个数增加,那么该边为桥

这就证明了割点和桥是没有关系的
点双连通分量 和 边双连通分量也是没有关系的

边双连通分量,时间戳,
dfn(u) low(u)
1.
如何找到桥
(假设x->y是一个桥)
那么 桥 <=> dfn(u) <low(y) 无法形成环
2.
如何找到所有的边的双连通分量
(1)将所有的桥删掉
(2)stack 类似于 有向图的强连通分量的处理方法

边的双连通分量算法

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 5010, M = 20100;

int n, m;
int dfn[N], low[N], timestamp;
int h[N], e[M], ne[M], idx;
int st[N], top;
bool is_b[N];
int d[N], dcc_cnt;
int id[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;
    st[top ++] = u;
    
    for(int i = h[u]; ~i ;i = ne[i]){
        int j = e[i];
        if(!dfn[j]){
            tarjan(j, u);
            low[u] = min(low[u], low[j]);
            if(dfn[u] < low[j]){
                is_b[i] = is_b[i ^ 1] = true;
            }
        }
        else if(j != from){
            low[u] = min(low[u], low[j]);
        }
    }
    if(dfn[u] == low[u]){
         ++ dcc_cnt;
        int j;
        do {
            j = st[-- top];
            id[j] = dcc_cnt;
        }while (j != u) ;
    }
    return ;
}

int main(){
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    
    while(m --){
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);
    }
    
    tarjan(1, -1);

    for(int i = 0;i < idx ;i ++){
        if(is_b[i])//如果是桥的话算度为1的点的个数,不是桥的话没有算的意义
            d[id[e[i]]] ++;
    }
    
    int cnt;
    for(int i = 1;i <= dcc_cnt; i ++){
        if(d[i] == 1)
            cnt ++;
    }
    printf("%d\n", (cnt + 1) /2);
    return 0;
}

点的双连通分量

删除该点后,剩余的连通块数量最大的值

也就是求一个连通块内,如果删除每一个点,然后剩余的连通块的数量的最大值

如果该点为头节点,那么删除该点后剩余的连通块的个数为该点的子树的个数,
如果该点不是头节点的话,那么剩余的连通块个数为,以该点为子树的个数和头节点为根节点的子树,

因为该图最开始的时候为多个连通块,所以还得统计未删点的时候的连通块的个数

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

using namespace std;

const int N = 10010, M = 30010;

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

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;
    
    int cnt = 0;//记录该节点为根节点 并 删除该节点之后子连通块的个数
    
    for(int i = h[u]; ~i; i = ne[i]){
        int j = e[i];
        if( !dfn[j] ){
            tarjan(j, u);
            low[u] = min(low[u], low[j]);
            if(dfn[u] <= low[j]) cnt ++;//该点为环嘴上面的点 或者 是一个子树
        }
            else if(j != from) low[u] = min(low[u], dfn[j]);// ?
    }
    
    if(u != root)
        cnt ++;
    ans = max(cnt, ans);
}

int main(){
    while(scanf("%d%d", &n, &m), n || m){
        
        memset(dfn, 0, sizeof dfn);
        memset(h, -1, sizeof h);
        idx = 0;
        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 ++)
            if(!dfn[root]){
                cnt ++;
                tarjan(root, -1);
            }
        
        printf("%d\n",ans + cnt - 1);
    }
    return 0;
}

点双连通分量

如果该图是一棵树的话,那么所有点都是点双连通分量

求点双连通分量和强连通分量是一个意思,

如果该点为该点双连通的最上面的点,那么其他点即使递归,在栈内,也不会被弹出栈,知道递归返回到双连通分量内时间戳最早的那个点,栈内的点到该点为同一点双连通分量

#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)
{
    dfn[u] = low[u] = ++ timestamp;
    stk[ ++ top] = u;

    if (u == root && h[u] == -1)
    {
        dcc_cnt ++ ;
        dcc[dcc_cnt].push_back(u);
        return;
    }

    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]);
            if (dfn[u] <= low[j])
            {
                cnt ++ ;
                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);
        }

        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]])
                    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;
}

这个代码为什么把加入栈的操作放在遍历边的里面呢,

这是因为同一个割点至少是连接两个点双连通分量的,如果在遍历完所有的边后加入的话那么这至少两个双连通分量就变成了一个,答案错误

割点并不用设置为是出口
一个割点至少连接两个点双连通分量,
如果割点被封死,那么点双连通分量内有出口
如果点双连通分量内被封死,那么可以通过割点去别的点双连通分量

点双连通分量有一个割点的话是有一个出口
点双连通分量有多个割点的话就不用出口了

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值