双连通分量 总结及例题

点双连通和边双连通

  • 连通的概念:在无向图中,所有点能互相到达
  • 连通分量:互相联通的子图
  • 点双连通:删掉一个点之后,图仍联通
  • 边双连通:删掉一条边之后,图仍联通

tarjan 算法:

该算法是R.Tarjan发明的。对图深度优先搜索, dfn[i]为第i个结点在搜索树中的深度,low[i]为第i个结点的子树的所有儿子连接到的最上面的结点层数。根据定义,则有:

Low(u)=Min
{
    dfn(u),
    dfn(v) ,(u,v)为后向边(返祖边) 等价于 DFS(v)<DFS(u)且v不为u的父亲节点
    Low(v) ,(u,v)为树枝边(父子边)
}

一个顶点u是割点,当且仅当满足(1)或(2)
(1) u为树根,且u有多于一个子树。
(2) u不为树根,且满足存在(u,v)为树枝边(或称父子边,即u为v在搜索树中的父亲),使得DFS(u)<=Low(v)。
一条无向边(u,v)是桥,当且仅当(u,v)为树枝边,且满足DFS(u)

求双连通分量

  1. 对于点双连通分量,实际上在求割点的过程中就能顺便把每个点双连通分量求出。建立一个栈,存储当前双连通分量,在搜索图时,每找到一条树枝边或后向边(非横叉边),就把这条边加入栈中。如果遇到某时满足DFS(u)<=Low(v),说明u是一个割点,同时把边从栈顶一个个取出,直到遇到了边(u,v),取出的这些边与其关联的点,组成一个点双连通分支。割点可以属于多个点双连通分量,其余点和每条边只属于且属于一个点双连通分量支。
    (这种还没有实现过,不过我认为显然如果把割点标记出来,跑dfs也能求出点双连通分支,虽然代码量会上升,不过还挺好打的,下面例题二类似)
  2. 对于边双连通分量,求法更为简单。只需在求出所有的桥以后,把桥边删除,原图变成了多个连通块,则每个连通块就是一个边双连通分量。桥不属于任何一个边双连通分量,其余的边和每个顶点都属于且只属于一个边双连通分量。

构造双连通图

  1. 一个有桥的连通图,如何把它通过加边变成边双连通图?方法为首先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1
  2. 统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf。则至少在树上添加(leaf+1)/2条边,就能使树达到边二连通,所以至少添加的边数就是(leaf+1)/2。具体方法为,首先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf+1)/2次,把所有点收缩到了一起。

模板

int root, cnt;
int vis[maxn], dfn[maxn], low[maxn];
bool cut[maxn];
//vector<pair<int, int>>bridge;

void dfs(int u, int fa)
{
    int son=0;
    vis[u]=1;
    dfn[u]=low[u]=++cnt;
    for (int i=0; i<G[u].size(); i++)
    {
        int v=G[u][i];
        if (v==fa) continue;
        if (vis[v]==1) low[u]=min(low[u], dfn[v]); //返祖边
        if (vis[v]==0)
        {
            dfs(v, u);
            son++;
            low[u]=min(low[u], low[v]);
            if ( (u==root && son>1) || (u!=root && low[v]>=dfn[u]))
            {
                cut[u]=true;
                //if(low[v] > dfn[u]) bridge.push_back({u, v}); //(u, v) 是桥
            }
        }
    }
    vis[u]=2;
}

void tarjan_init()
{
    memset(vis, 0, sizeof(vis));
    memset(cut, 0, sizeof(cut));
    cnt=0; root=1;
    //bridge.clear();
}

例题

POJ1144【基础】
题目大意:
给出一个无向图,求出有多少个割点。
输入:
有若干组测试数据。每一组测试数据的第一行有一个整数 n,表示有 n
(1<=n<100)个点,n=0 时测试数据结束。接下来有若干行,每一行第一个整
数 u 表示这一行描述的是以 u 为起点的边,接下来有若干个整数 vi 表示有一条
边 u-vi,u=0 时表示这一组测试数据结束。
输出:
对于每一组测试数据,输出一个整数,即有多少个割点。

模板题,没太多要说的

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstdlib>
#include<cstring>
using namespace std;
typedef long long LL;
const int maxn=110;
vector<int> G[maxn], a;
char s[1000];
void add(int u, int v)
{
    G[u].push_back(v);
    G[v].push_back(u);
}

void deal()
{
    a.clear();
    int lens=strlen(s), now=0;
    for (int i=0; i<lens; i++)
        if (s[i]==' ')
        {
            a.push_back(now);
            now=0;
        }else now=now*10+(s[i]-'0');
    a.push_back(now);
}

//tarjan
int root, cnt;
int vis[maxn], dfn[maxn], low[maxn];
bool cut[maxn];
//vector<pair<int, int>>bridge;

void dfs(int u, int fa)
{
    int son=0;
    vis[u]=1;
    dfn[u]=low[u]=++cnt;
    for (int i=0; i<G[u].size(); i++)
    {
        int v=G[u][i];
        if (v==fa) continue;
        if (vis[v]==1) low[u]=min(low[u], dfn[v]);
        if (vis[v]==0)
        {
            dfs(v, u);
            son++;
            low[u]=min(low[u], low[v]);
            if ( ((u==root) && son>1) || (u!=root && low[v]>=dfn[u]))
            {
                cut[u]=true;
                //if(low[v] > dfn[u]) bridge.push_back({u, v}); //(u, v) 是桥
            }
        }
    }
    vis[u]=2;
}

void tarjan_init()
{
    memset(vis, 0, sizeof(vis));
    memset(cut, 0, sizeof(cut));
    cnt=0; root=1;
    //bridge.clear();
}

int main()
{
    int n;
    while (scanf("%d\n", &n)!=EOF)
    {
        if (n==0) break;
        for (int i=1; i<=n; i++) G[i].clear();
        int u, v;
        while (1)
        {
            scanf("%[^\n]s", s); getchar();
            deal(); 
            for (int i=1; i<a.size(); i++) add(a[0], a[i]);
            if (a[0]==0) break; 
        }

        tarjan_init();
        dfs(1, -1);

        int ans=0;
        for (int i=1; i<=n; i++)
            if (cut[i]) ans++;
        printf("%d\n", ans);
    }
    return 0;
}

POJ1523

题目大意:
给出一个无向图,求出其割点的数量,并且求出去掉每一个割点后原图分成多
少个连通分量。
输入:
有若干组测试数据。每一组测试数据有若干行,每一行有两个整数表示无向图
中的一条边,或者一个 0 表示这一组测试数据的结束。所有测试数据最后以一
个 0 作为结束。无向图中的点的数量不超过 1000 个,假定一个图里面的点从 1
开始编号,并且是连续递增的。
输出:
对于每一组测试数据,如果有割点,按如下格式输出:
Network #测试数据组序号
SPF node 割点 1 编号 leaves x1 subnets
SPF node 割点 2 编号 leaves x2 subnets
……
其中 xi 表示在图中去掉某一个割点后产生的连通分量数量。
如果没有割点,输出:
Network #测试数据组序号
No SPF nodes
两组输出之间需要有一个空行进行分割。

题解:
先求割点,然后枚举每一个割点裸 dfs 求连通分量数量。

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstdlib>
#include<cstring>
using namespace std;
typedef long long LL;
const int maxn=1010;
vector<int> G[maxn+10];
void add(int u, int v)
{
    G[u].push_back(v);
    G[v].push_back(u);
}

//tarjan
int root, cnt;
int vis[maxn], dfn[maxn], low[maxn];
bool cut[maxn];
//vector<pair<int, int>>bridge;

void dfs(int u, int fa)
{
    int son=0;
    vis[u]=1;
    dfn[u]=low[u]=++cnt;
    for (int i=0; i<G[u].size(); i++)
    {
        int v=G[u][i];
        if (v==fa) continue;
        if (vis[v]==1) low[u]=min(low[u], dfn[v]);
        if (vis[v]==0)
        {
            dfs(v, u);
            son++;
            low[u]=min(low[u], low[v]);
            if ( ((u==root) && son>1) || (u!=root && low[v]>=dfn[u]))
            {
                cut[u]=true;
                //if(low[v] > dfn[u]) bridge.push_back({u, v}); //(u, v) 是桥
            }
        }
    }
    vis[u]=2;
}

void tarjan_init()
{
    memset(vis, 0, sizeof(vis));
    memset(cut, 0, sizeof(cut));
    cnt=0; root=1;
    //bridge.clear();
}
int n;
int belong[maxn], cnt2;

void dfs2(int u, int fa, int x)
{
    if (belong[u]!=-1) return; 
    belong[u]=cnt2;
    for (int i=0; i<(int)G[u].size(); i++)
    {
        int v=G[u][i];
        if (v==fa || v==x) continue;
        dfs2(v, u, x);
    }
}

void deal(int x)
{
    memset(belong, -1, sizeof(belong));
    cnt2=0;
    for (int i=1; i<=n; i++) 
        if (i!=x && belong[i]==-1) ++cnt2, dfs2(i, -1, x);
}


int main()
{
    int a, b, kase=0;
    while (scanf("%d", &a)!=EOF)
    {
        if (a==0) break;
        kase++;
        printf("Network #%d\n", kase);
        for (int i=1; i<=1000; i++) G[i].clear();
        scanf("%d", &b);
        n=max(a, b);
        add(a, b);
        int u, v;
        while (scanf("%d", &u)!=EOF)
        {
            if (u==0) break;
            scanf("%d", &v);
            add(u, v);
            n=max(n, u); n=max(n, v);
        }
        tarjan_init();
        dfs(1, -1);
        bool ans=false;
        for (int i=1; i<=n; i++) if (cut[i]) ans=true; 
        if (ans==false)
            printf("  No SPF nodes\n");
        else
            for (int i=1; i<=n; i++)
            {
                deal(i);
                if (cut[i]) printf("  SPF node %d leaves %d subnets\n", i, cnt2);
            }
        printf("\n");
    }
    return 0;
}

POJ3694

题目大意:
给出一个无向图,有 N 个点和 M 条边( 1<=N<=100000, N-1<=M<=200000)求出
有多少条割边(桥),以及每加入一条新的边以后还剩下多少条割边。假定所
有的点从 1 到 N 进行编号,一开始的时候全图是连通的。
输入:
有若干组测试数据。每一组测试数据的第一行有两个整数 N 和 M,当 N=M=0 时
输入数据结束。接下来有 M 行,每一行有两个整数 u, v,表示在 u, v 之间有
一条边。接下来有一行,包含一个整数 Q,表示将加入多少条边。接下来有 Q
行,每一行按上述相同方式描述一条边。
输出:
对于每一组测试数据,先输出一行“ Case %测试数据组编号%:”,然后对每一
条新加入的边,输出一行,包含一个整数,即加入了这一条边以后还有多少条
割边。

问题分析:
如下图,如若加了绿边,带来的影响是从绿边(u,v)往上的节点,直到lca(u,v)都不再是割点
这里写图片描述
用并查集处理即可,这里的lca不用写O(log n)的版本,O(n)的即可

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstdlib>
#include<cstring>
using namespace std;
typedef long long LL;
const int maxn=100100;
vector<int> G[maxn+10];
void add(int u, int v)
{
    G[u].push_back(v);
    G[v].push_back(u);
}
//tarjan
int root, cnt, ans;
int vis[maxn], dfn[maxn], low[maxn];
bool bridge[maxn];
int parent[maxn];
void dfs(int u, int fa)
{
    int son=0;
    vis[u]=1;
    dfn[u]=low[u]=++cnt;
    for (int i=0; i<(int)G[u].size(); i++)
    {
        int v=G[u][i];
        if (v==fa) continue;
        if (vis[v]==1) low[u]=min(low[u], dfn[v]); 
        if (vis[v]==0)
        {
            dfs(v, u);
            parent[v]=u;
            son++;
            low[u]=min(low[u], low[v]);
            if ( (u==root && son>1) || (u!=root && low[v]>=dfn[u]) )
            {
                ans++;
                bridge[v]=1;
            }
        }
    }
    vis[u]=2;
}

void tarjan_init()
{
    memset(vis, 0, sizeof(vis));
    memset(bridge, 0, sizeof(bridge));
    cnt=0; root=1; ans=0;
}


void lca(int u, int v)
{
    if (dfn[v]<dfn[u]) swap(u, v);
    while (dfn[v]>dfn[u])
    {
        if(bridge[v]) {
            ans--;
            bridge[v] = 0;
        }
        v=parent[v];
    }
    while(u!= v)
    {
        if (bridge[u]) {
            ans--;
            bridge[u]=0;
        }
        u=parent[u]; v=parent[v];
    }
}

int main()
{
    int n, m, kase=0;
    while (scanf("%d%d", &n, &m)!=EOF)
    {
        if (n==0 && m==0) break;
        printf("Case %d:\n", ++kase);
        for (int i=1; i<=m; i++)
        {
            int u, v;
            scanf("%d%d", &u, &v);
            add(u, v);
        }
        tarjan_init();
        dfs(1, -1);
        int Q;
        scanf("%d", &Q);
        while (Q--)
        {
            int u, v;
            scanf("%d%d", &u, &v);
            lca(u, v);
            printf("%d\n", ans);
        }
        printf("\n");
    }
    return 0;
}
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值