AcWing 算法提高课 无向图的双联通分量

一、定义:

 

1、极大的不含有桥(割边)的连通块称为边双连通分量。

性质:

(1)边双连通分量,不管删掉哪条边,还是连通的

(2)任意两点间都有两条(边)不相交的路径

2、极大的不含有割点的连通块称为点双连通分量。

性质:

(1)每个割点至少属于两个点双连通分量

(2)割点和割边没什么关系

二、求解方法

1、Tarjan算法求边双连通分量:   

 模板:(同时记录桥和双连通分量)

复制代码
const int N=5010;
const int M=20010;
vector<int> adj[N];
vector<int> idx[N];//边的编号
int dfn[N],low[N],timestamp;
stack<int> stk;
int id[N],dcc_cnt;
bool is_bridge[M];
int n,m;
void Tarjan(int u,int from)
{
    dfn[u]=low[u]=++timestamp;
    stk.push(u);
    
    for(int i=0;i<adj[u].size();i++)
    {
        int nxt=adj[u][i];
        int e=idx[u][i];
        if(!dfn[nxt])
        {
            Tarjan(nxt,e);
            low[u]=min(low[u],low[nxt]);
            if(dfn[u]<low[nxt])
            {
                is_bridge[e]=is_bridge[e^1]=true;
                
            }
        }
        else if(e!=(from^1))
        {
            low[u]=min(low[u],dfn[nxt]);
        }
    }
    if(dfn[u]==low[u])
    {
        ++dcc_cnt;
        int v;
        do
        {
            v=stk.top();
            stk.pop();
            id[v]=dcc_cnt;
        } while(v!=u);
    }
}
void YD()
{
    cin>>n>>m;
    int cnt=0;
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        adj[a].pub(b);
        idx[a].pub(cnt++);
        adj[b].pub(a);
        idx[b].pub(cnt++);
    }
    Tarjan(1,-1);
    
}
View Code
复制代码

 2、Tarjan算法求点双连通分量

(1)求割点

 

 模板:(求割点,并计算去掉割点后,此割点所处的连通块会变成几个块)

复制代码
int n,m;
const int N=10010;
vector<int> adj[N];
int dfn[N],low[N],timestamp;
int root,ans;
void Tarjan(int u)
{
    dfn[u]=low[u]=++timestamp;
    int cnt=0;
    for(auto nxt:adj[u])
    {   
        if(!dfn[nxt])
        {
            Tarjan(nxt);
            low[u]=min(low[u],low[nxt]);
            if(low[nxt]>=dfn[u])
            {
                cnt++;
            }
        }
        else low[u]=min(low[u],dfn[nxt]);
    }
    
    if(u!=root&&cnt>0)
    {
        cnt++;
    }
    
    ans=max(ans,cnt);
}
void YD()
{
    fore(i,1,n) adj[i].clear();
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    timestamp=0;
    
    while(m--)
    {
        int a,b;cin>>a>>b;
        a++,b++;
        adj[a].push_back(b);
        adj[b].push_back(a);
    }
    int cnt=0;
    ans=0;
    fore(i,1,n)
    {
        if(!dfn[i])
        {
            root=i;
            cnt++;
            Tarjan(i);
        }
    }
    
    cout<<ans+cnt-1<<endl;
}
View Code
复制代码

Tarjan()中的cnt表示去掉当前割点后会变为几个连通块,YD()中的cnt是连通块的个数

 (2)求点双连通分量

 

 点双连通分量的缩点方式:

 

 

 求点双连通分量的模板:

复制代码
const int N=1010;
int n,m;
vector<int> adj[N];
stack<int> stk;
int dfn[N],low[N],timestamp;
int dcc_cnt;
vector<int> dcc[N];//存储双连通分量中的点
bool cut[N];
int root;
void Tarjan(int u)
{
    dfn[u]=low[u]=++timestamp;
    stk.push(u);
    
    if(root==u&&adj[u].size()==0)
    {
        dcc_cnt++;
        dcc[dcc_cnt].pub(u);
        return;
    }
    
    int cnt=0;//分支数
    for(auto nxt:adj[u])
    {
        if(!dfn[nxt])
        {
            Tarjan(nxt);
            low[u]=min(low[u],low[nxt]);
            if(dfn[u]<=low[nxt])
            {
                cnt++;
                //根且分支数大于等于2,非根分支数大于等于1,就是割点。
                if(u!=root||cnt>1) cut[u]=true;
                ++dcc_cnt;
                int v;
                do
                {
                    v=stk.top();
                    stk.pop();
                    dcc[dcc_cnt].pub(v);
                } while(v!=nxt);//注意判断条件
                dcc[dcc_cnt].pub(u);//还要将当前点放入
            }
        }
        else
        {
            low[u]=min(low[u],dfn[nxt]);
        }
    }
}
void YD()
{
    ii++;
    timestamp=0;dcc_cnt=0;n=0;
    while(!stk.empty()) stk.pop();
    fore(i,1,N)
    {
        adj[i].clear();
        dcc[i].clear();
    }
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    memset(cut,0,sizeof(cut));
    
    while(m--)
    {
        int a,b;cin>>a>>b;
        n=max({n,a,b});
        adj[a].pub(b);
        adj[b].pub(a);
    }
    for(root=1;root<=n;root++)
    {
        if(!dfn[root])
        {
            Tarjan(root);
        }
    }
View Code
复制代码

三、例题

1、给定一个无向连通图,问最少加几条边,可以将其变成一个边双连通分量

(1)获取全部边双连通分量

(2)将边双连通分量缩点,此时图上的所有边都是桥,即变为一棵树

 (3)度为1的点都需要加一条边,设有cnt个度为1的点,故需要加[cnt/2]条边(上取整),即(cnt+1)/2(整数除法)

例题:https://www.acwing.com/problem/content/397/

代码:

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

#define fore(x,y,z) for(LL x=(y);x<=(z);x++)
#define forn(x,y,z) for(LL x=(y);x<(z);x++)
#define rofe(x,y,z) for(LL x=(y);x>=(z);x--)
#define rofn(x,y,z) for(LL x=(y);x>(z);x--)
#define pub push_back
#define all(x) (x).begin(),(x).end()
#define fi first
#define se second

using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
typedef pair<LL,LL> PLL;
const int N=5010;
const int M=20010;
vector<int> adj[N];
vector<int> idx[N];//边的编号
int dfn[N],low[N],timestamp;
stack<int> stk;
int id[N],dcc_cnt;
bool is_bridge[M];
int n,m;
int d[N];//缩点后的度数
void Tarjan(int u,int from)
{
    dfn[u]=low[u]=++timestamp;
    stk.push(u);
    
    for(int i=0;i<adj[u].size();i++)
    {
        int nxt=adj[u][i];
        int e=idx[u][i];
        if(!dfn[nxt])
        {
            Tarjan(nxt,e);
            low[u]=min(low[u],low[nxt]);
            if(dfn[u]<low[nxt])
            {
                is_bridge[e]=is_bridge[e^1]=true;
                
            }
        }
        else if(e!=(from^1))
        {
            low[u]=min(low[u],dfn[nxt]);
        }
    }
    if(dfn[u]==low[u])
    {
        ++dcc_cnt;
        int v;
        do
        {
            v=stk.top();
            stk.pop();
            id[v]=dcc_cnt;
        } while(v!=u);
    }
}
void YD()
{
    cin>>n>>m;
    int cnt=0;
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        adj[a].pub(b);
        idx[a].pub(cnt++);
        adj[b].pub(a);
        idx[b].pub(cnt++);
    }
    Tarjan(1,-1);
    fore(i,1,n)
    {
        for(int k=0;k<adj[i].size();k++)
        {
            int j=adj[i][k];
            int e=idx[i][k];
            if(is_bridge[e])
            {
                d[id[i]]++;
            }
        }
    }
    int res=0;
    fore(i,1,dcc_cnt)
    {
        if(d[i]==1) res++;
    }
    cout<<(res+1)/2<<endl;
}
 
int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int T=1;
    //cin >> T;
    while (T--)
    {
        YD();
    }
    return 0;
}
View Code
复制代码

2、删除一个点之后剩余的连通块最多有多少

(1)求一共有几个连通块

(2)求去掉一个割点最多会产生几个新的连通块

例题:https://www.acwing.com/problem/content/1185/

代码:

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

#define fore(x,y,z) for(LL x=(y);x<=(z);x++)
#define forn(x,y,z) for(LL x=(y);x<(z);x++)
#define rofe(x,y,z) for(LL x=(y);x>=(z);x--)
#define rofn(x,y,z) for(LL x=(y);x>(z);x--)
#define pub push_back
#define all(x) (x).begin(),(x).end()
#define fi first
#define se second

using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
typedef pair<LL,LL> PLL;
int n,m;
const int N=10010;
vector<int> adj[N];
int dfn[N],low[N],timestamp;
int root,ans;
void Tarjan(int u)
{
    dfn[u]=low[u]=++timestamp;
    int cnt=0;
    for(auto nxt:adj[u])
    {   
        if(!dfn[nxt])
        {
            Tarjan(nxt);
            low[u]=min(low[u],low[nxt]);
            if(low[nxt]>=dfn[u])
            {
                cnt++;
            }
        }
        else low[u]=min(low[u],dfn[nxt]);
    }
    
    if(u!=root&&cnt>0)
    {
        cnt++;
    }
    
    ans=max(ans,cnt);
}
void YD()
{
    fore(i,1,n) adj[i].clear();
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    timestamp=0;
    
    while(m--)
    {
        int a,b;cin>>a>>b;
        a++,b++;
        adj[a].push_back(b);
        adj[b].push_back(a);
    }
    int cnt=0;
    ans=0;
    fore(i,1,n)
    {
        if(!dfn[i])
        {
            root=i;
            cnt++;
            Tarjan(i);
        }
    }
    
    cout<<ans+cnt-1<<endl;
}
 
int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    

    while (cin>>n>>m,n||m)
    {
        YD();
    }
    return 0;
}
View Code
复制代码

 3、求设置几个出口可以使某一个点坍塌后,其他点依旧可以连通到出口

(1)若只有一个点的连通块,数量加1

(2)否则,求连通块的点双连通分量,此时若此连通块就是一个点双连通分量,即没有割点,需要出口数加2

(2)否则,需要在度为1的点双连通分量(即只有一个割点)内部需要设置一个出口

然后将数量加起来,方案数乘起来。

代码:

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

#define fore(x,y,z) for(LL x=(y);x<=(z);x++)
#define forn(x,y,z) for(LL x=(y);x<(z);x++)
#define rofe(x,y,z) for(LL x=(y);x>=(z);x--)
#define rofn(x,y,z) for(LL x=(y);x>(z);x--)
#define pub push_back
#define all(x) (x).begin(),(x).end()
#define fi first
#define se second

using namespace std;
typedef unsigned long long LL;
typedef pair<int,int> PII;
typedef pair<LL,LL> PLL;
int ii=0;
const int N=1010;
int n,m;
vector<int> adj[N];
stack<int> stk;
int dfn[N],low[N],timestamp;
int dcc_cnt;
vector<int> dcc[N];//存储双连通分量中的点
bool cut[N];
int root;
void Tarjan(int u)
{
    dfn[u]=low[u]=++timestamp;
    stk.push(u);
    
    if(root==u&&adj[u].size()==0)
    {
        dcc_cnt++;
        dcc[dcc_cnt].pub(u);
        return;
    }
    
    int cnt=0;//分支数
    for(auto nxt:adj[u])
    {
        if(!dfn[nxt])
        {
            Tarjan(nxt);
            low[u]=min(low[u],low[nxt]);
            if(dfn[u]<=low[nxt])
            {
                cnt++;
                //根且分支数大于等于2,非根分支数大于等于1,就是割点。
                if(u!=root||cnt>1) cut[u]=true;
                ++dcc_cnt;
                int v;
                do
                {
                    v=stk.top();
                    stk.pop();
                    dcc[dcc_cnt].pub(v);
                } while(v!=nxt);//注意判断条件
                dcc[dcc_cnt].pub(u);//还要将当前点放入
            }
        }
        else
        {
            low[u]=min(low[u],dfn[nxt]);
        }
    }
}
void YD()
{
    ii++;
    timestamp=0;dcc_cnt=0;n=0;
    while(!stk.empty()) stk.pop();
    fore(i,1,N)
    {
        adj[i].clear();
        dcc[i].clear();
    }
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    memset(cut,0,sizeof(cut));
    
    while(m--)
    {
        int a,b;cin>>a>>b;
        n=max({n,a,b});
        adj[a].pub(b);
        adj[b].pub(a);
    }
    for(root=1;root<=n;root++)
    {
        if(!dfn[root])
        {
            Tarjan(root);
        }
    }
    int res=0;//最小个数
    LL num=1;//方案数
    fore(i,1,dcc_cnt)
    {
        int cnt=0;
        for(auto u:dcc[i])
        {
            if(cut[u])
            {
                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;
        }
    }
    cout<<"Case "<<ii<<": "<<res<<' '<<num<<endl;
}
 
int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    while (cin>>m,m)
    {
        YD();
    }
    return 0;
}
View Code
复制代码

 

posted @   80k  阅读(57)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示