无向图的双连通分量

无向图的双连通分量

一些定义和性质:

对于一张连通的无向图,如果删除图中的某条边之后原先的图不能成为连通图,那么这条边就被称为

极大的不包含桥的连通块,被称为 边双连通分量(e-dcc)

对于边双连通分量,不管删除哪条边,图依旧是连通的。

类似于桥,如果删除图中的某个点与和他连的边之后原先的图不能成为连通图,那么这个点就被称为

割点

极大的不包含割点的连通块,被称为 点双连通分量(v-dcc)

对于一个连通块 u u u ,如果不存在另一个连通块 v v v ,使得 v v v 包含 u u u 中所有的元素并且 v v v 含有 u u u 中没有的元素,称 u u u极大的

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

对于一棵树,所有边都是桥

每一个点双连通分量至少包含一个割点。

两个割点之间的边不一定是桥。

1637471113762

一个桥连接的两个端点不一定是割点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ZEYljky-1637498558202)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637471162436.png)]

点双连通分量不一定是边双连通分量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c53Z6rEI-1637498558203)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637471242860.png)]

边双连通分量不一定是点双连通分量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EzlGbgDv-1637498558205)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637471282559.png)]

一言蔽之,割点和桥 没联系。


边双连通分量

理论实现

求解边双连通分量,类似于有向图的强连通分量问题,再次引入时间戳 的概念。

定义 d f n ( x ) dfn(x) dfn(x) d f s dfs dfs 序时第一次访问到 x x x 节点的时间点

定义 l o w ( x ) low(x) low(x) 为以 x x x 节点为根节点的连通分量最早能到达的时间点

无向图不存在横叉边,因为在无向图中,横叉边必然在搜索前一个点是遍历完成,此时应该是前向边。

  • 如何判断桥?

    判断 y y y 能否走到 x x x 的祖先节点!!!如果不能,则为桥。

    $dfn(x)<low(y) $

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

    利用栈维护,如果 d f n ( x ) = = l o w ( x ) dfn(x)==low(x) dfn(x)==low(x) ,表示 x x x 无法返回他的祖先节点,说明 x x x 的父节点与 x x x 的边即为桥,还在栈当中的节点即为边双连通分量。

写的时候因为是无向边,我们要新加一个参数 p r e pre pre ( p r e pre pre 是边不是点)防止走回头路,因为如果走回头路的话必然找不到桥了。

要新开一个数组 b r i d g e [ M A X N ] bridge[MAXN] bridge[MAXN] 来记录每条边是否是桥。如果一条边是桥,那么他的反向边也是桥。

模板例题

边双连通分量模板题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H62pZy32-1637498558207)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637478440698.png)]

给出一个无向的连通图,问最少加几条边能使整张图变成边双连通分量。

**结论:**对于一个无向连通图,将图中所有的双连通分量缩点后,定义那些度数为 1 1 1 的点个数为 c n t cnt cnt 。则最少需要添加 ( c n t + 1 ) / 2 (cnt+1)/2 (cnt+1)/2 条边,就能使原图变为双连通分量。不会证明

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 5005
#define MAXM 200005
typedef pair<int,int> pii;
#define INF 0x3f3f3f3f
int n,m;
int num,dfn[MAXN],low[MAXN],dcc,indgr[MAXN],key[MAXN],vis[MAXN];
stack <int> st;

int head[MAXN];int tot;
struct EDGE
{
    int to,next;
}edge[MAXM];
void add_edge(int from,int to)
{
    edge[++tot].to=to;edge[tot].next=head[from];head[from]=tot;
}
int bridge[MAXM];//判断某条边是不是桥
int get(int x)//根据边的编号求它的反向边
{
    if(x&1) return x+1;
    return x-1;
}

void tarjan(int x,int pre)
{
    dfn[x]=low[x]=++num;
    st.push(x);vis[x]=1;
    for(int i=head[x];i;i=edge[i].next)
    {
        int y=edge[i].to;
        if(!dfn[y])
        {
            tarjan(y,i);//是从i这一条边遍历过来的
            low[x]=min(low[x],low[y]);
            if(dfn[x]<low[y])//y无论如何都回不到y的祖先节点,那么这条边和其反向边为桥
            {
                bridge[i]=bridge[get(i)]=1;
            }
        }
        else if(i!=get(pre))//这条边不是走回头路的,且y之前已经记录过它的时间戳
        {
            low[x]=min(low[x],dfn[y]);
        }
    }
    if(dfn[x]==low[x])
    {
        dcc++;
        int now=-1;
        while(now!=x)
        {
            now=st.top();st.pop();
            vis[now]=0;
            key[now]=dcc;
        }
    }
}

void solve()
{
    cin>>n>>m;

    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        add_edge(u,v),add_edge(v,u);
    }
    tarjan(1,0);
    for(int i=1;i<=tot;i++)
        if(bridge[i])
            indgr[key[edge[i].to]]++;
    int ans=0;
    for(int i=1;i<=dcc;i++)
        if(indgr[i]==1)
            ans++;
    cout<<(ans+1)/2<<endl;
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

之前济南热身赛有一道曹操炸桥的题。题意很明显,要炸的桥就是边双连通分量意义下的桥。我们只需找到边权最小的桥把它炸掉即可。


点双连通分量

理论实现

依旧引入 时间戳 的概念

  • 如何求割点?

还是类似,由 x x x 连向 y y y ,去掉 x x x 和与之相连的边后, y y y 不能返回 x x x 的祖先节点。但如果 x x x 是整个图的根节点,那么还得继续讨论

  1. x x x 不是根节点, d f n [ x ] ≤ l o w [ y ] dfn[x] \leq low[y] dfn[x]low[y]
  2. x x x 是根节点,满足 x x x 至少有两个子节点 y 1 , y 2 y_1,y_2 y1,y2 ,使得$low[y_1] \geq dfn[x] $ and $ low[y_2] \geq dfn[x]$
  • 如何维护所有点双连通分量

还是需要一个栈。

当发现 d f n [ x ] ≤ l o w [ y ] dfn[x] \leq low[y] dfn[x]low[y] 时,说明 y y y 无法返回 x x x 的祖先节点,说明原图中点双连通分量的个数 v c c vcc vcc 加一。如果 x x x 不是根节点或者 c n t > 1 cnt>1 cnt>1 ,说明 x x x 是割点。此时就将栈中元素弹出直至弹出 y y y 。并且 x x x 也属于该点双连通分量。

特判一下一个孤立点的情况,因为这也算一个点双连通分量。

模板例题

第一道例题,只要判断割点就行,无需维护边双连通分量。

判连通块个数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SAQUGS9q-1637498558208)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637482810818.png)]

统计连通块个数 $cnt $

枚举从各个连通块中删除某个点 ,原先的一个连通块变成了 s s s 个连通块

答案即为 m a x ( c n t + s − 1 ) max(cnt +s-1) max(cnt+s1)

关键在于找删除某个点后新生成的连通块个数

  • 删除的点不是割点,不用管
  • 删除的点是割点,且是根节点,删除后分裂的个数即为 d f n [ x ] ≤ l o w [ y ] dfn[x] \leq low[y] dfn[x]low[y] 的个数
  • 如果不是根节点,分裂的个数为 d f n [ x ] ≤ l o w [ y ] dfn[x] \leq low[y] dfn[x]low[y] 的个数加一。因为是从根节点过来的,但遍历的时候不会去根节点。

问题转化为依次删除每个割点,求全局最大值。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 10005
#define MAXM 30005
int n,m,num,cnt,ans,root;
int low[MAXN],dfn[MAXN];
int head[MAXN];int tot;
struct EDGE
{
    int to,next;
}edge[MAXM];
void add_edge(int from,int to)
{
    edge[++tot].to=to;edge[tot].next=head[from];head[from]=tot;
}

void init()
{
    memset(dfn,0,sizeof(dfn));
    memset(head,-1,sizeof(head));
    tot=0,num=0,ans=0,cnt=0;
}

void tarjan(int x)
{
    dfn[x]=low[x]=++num;
    int cnt=0;//删除该点,原先的一个连通块变成cnt个连通块
    for(int i=head[x];i!=-1;i=edge[i].next)
    {
        int y=edge[i].to;
        if(!dfn[y])
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if(low[y]>=dfn[x]) cnt++;//x是割点,并且对于y,删除x后y不能与其祖先节点相连,多一个连通块
        }
        else low[x]=min(low[x],dfn[y]);
    }
    if(x!=root) cnt++;//x不是根节点的话祖先也得分出来一块
    ans=max(ans,cnt);
}

void solve()
{
    init();
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        add_edge(u,v),add_edge(v,u);
    }
    for(int i=0;i<n;i++)
    {
        if(!dfn[i])
        {
            root=i;
            cnt++;//连通块数量加一
            tarjan(i);//以i为根节点去遍历
        }
    }
    cout<<ans+cnt-1<<endl;
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    while(cin>>n>>m)
    {
        if(n==0&&m==0) break;
        solve();
    }
    return 0;
}

例题二

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XP7DBnrA-1637498558209)(C:\Users\ADguy\AppData\Roaming\Typora\typora-user-images\1637491971780.png)]

首先需要明确一个特判:所有出口数量 ≥ \geq 2

由于图不一定连通,可以遍历每个连通块,算出每个连通块内各自的方案。将他们乘起来后得到的值就是最后的答案。

如果一个连通块内无割点,那么任意选两个点作为出口即可。方案数量为 C c n t 2 C_{cnt}^2 Ccnt2

如果一个连通块内有割点,考虑缩点。

对于一个割点,他至少属于两个点双连通分量,很几把扯淡。所以写起来贼几把烦。

缩点步骤:

  1. 每个割点单独作为一个点
  2. 每个点双连通分量向它所包含的那个割点连条边

这样操作完之后,每个点双连通分量的度数就是其所连接的割点的个数。

对于缩完点之后的图,可以把割点看成连接各个点双连通分量的出口

对于一个点双连通分量里面来说,如果这个点双连通分量只连接了一个出口,即度数为 1 1 1 ,并且这个唯一的出口没了,那么这个点双连通分量里面必须确保有一个出口。方案数为 s i z e − 1 size-1 size1 s i z e size size 表示这个点双连通分量里点的个数,减一是因为这个出口不能是割点。

如果缩完点后的一个点双连通分量的度数大于 1 1 1 ,那么无论哪个出口塌了都可以逃出去,不用设置方案。

缩完点后的图必然是一棵树。度数为 1 1 1 的节点意味着是 叶子节点 ,因此加入树根没了,也就是割点,树的每棵子树必然可以逃到叶子节点避难。如果是其他点倒了,那就更没事了,因为可以逃到别的地方去避难或者自救。因此验证了方案的可靠性。

理一下思路:

  1. 求所有点双连通分量
  2. 缩点
  3. 统计度数(割点)
  4. 如果没有割点,方案 KaTeX parse error: Undefined control sequence: \C at position 1: \̲C̲_{size}^2,出口数量加 2 2 2 (特判孤立点的情况,方案贡献为 0 0 0 ,出口数量加 1 1 1);如果割点数量为1,方案 s i z e − 1 size-1 size1,出口数量加 1 1 1 ;如果割点数量大于1,对方案没有贡献,对出口数量没有贡献。
#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define MAXN 1005
#define MAXM 1005
int n,m,T;
int dfn[MAXN],num,low[MAXN],dcc,iscut[MAXN],root;
stack <int> st;
vector <int> dccc[MAXN];
int head[MAXN];int tot;
struct EDGE
{
    int to,next;
}edge[MAXM];
void add_edge(int from,int to)
{
    edge[++tot].to=to;edge[tot].next=head[from];head[from]=tot;
}

void init()
{
    for(int i=1;i<=dcc;i++) dccc[i].clear();
    memset(head,0,sizeof(head));
    tot=num=dcc=n=0;
    memset(dfn,0,sizeof(dfn));
    memset(iscut,0,sizeof(iscut));
    
}

void tarjan(int x)
{
    dfn[x]=low[x]=++num;
    st.push(x);
    if(x==root&&head[x]==0)//特判孤立点
    {
        dcc++;
        dccc[dcc].emplace_back(x);
        return;
    }
    int cnt=0;//统计裂开的块的的个数
    for(int i=head[x];i;i=edge[i].next)
    {
        int y=edge[i].to;
        if(!dfn[y])
        {
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if(dfn[x]<=low[y])//y无法到达x的祖先
            {
                cnt++;
                if(x!=root||cnt>1) 
                    iscut[x]=1;//x是割点
                dcc++;
                int now=-1;
                while(now!=y)
                {
                    now=st.top();st.pop();
                    dccc[dcc].emplace_back(now);
                }
                dccc[dcc].emplace_back(x);//把x也要push进去
            }
        }
        else    
            low[x]=min(low[x],dfn[y]);
    }
}

void solve()
{
    init();
    for(int i=1;i<=m;i++)
    {
        int a,b;
        cin>>a>>b;
        n=max({a,b,n});
        add_edge(a,b),add_edge(b,a);
    }
    for(root =1;root <=n;root++)
    {
        if(!dfn[root])
            tarjan(root);
    }
    int mx=0;
    ull ans=1;
    for(int i=1;i<=dcc;i++)
    {
        int cnt=0;//统计一个点双连通分量内的割点个数
        for(int j=0;j<dccc[i].size();j++)
        {
            if(iscut[dccc[i][j]]==1)//如果是割点
                cnt++;
        }
        if(cnt==0)
        {
            if(dccc[i].size()>1)
                mx+=2,ans*=dccc[i].size()*(dccc[i].size()-1)/2;
            else
                mx++;
        }
        else if(cnt==1)
            mx++,ans*=(dccc[i].size()-1);
    }
    cout<<"Case "<<T<<": "<<mx<<" "<<ans<<endl;
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    T=0;
    while(cin>>m)
    {
        if(!m) break;
        T++;
        solve();
    }

    return 0;
}

size()>1)
mx+=2,ans*=dccc[i].size()(dccc[i].size()-1)/2;
else
mx++;
}
else if(cnt==1)
mx++,ans
=(dccc[i].size()-1);
}
cout<<"Case “<<T<<”: “<<mx<<” "<<ans<<endl;
}

int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
T=0;
while(cin>>m)
{
if(!m) break;
T++;
solve();
}

return 0;

}



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值