【算法提高课】图论:无向图的双连通分量


title: 【算法提高课】图论:无向图的双连通分量
katex: true
tags:

  • Acwing
  • hard
  • 图论
    categories: 算法提高课

概念

  • 桥: 在一个图中,如果断开该边,两边会不连通,该边称为桥
  • 边双连通分量(e-DCC): 不含桥的联通区域。不管删掉那条边,图还是联通的。极大的不包含桥的连通块。
  • 边双连通(oiwiki): 在一张连通的无向图中,对于两个点 u u u v v v,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 u u u v v v 边双连通。
  • 割点: 如果删掉这个点所相连的边,整个图会变得不连通,该点称为割点。(删掉这个点)
  • 点双连通分量(v-DCC): 极大的不包含割点的双连通分量
  • 点双连通(oiwiki): 在一张连通的无向图中,对于两个点 u u u v v v,如果无论删去哪个点(只能删去一个,且不能删 u u u v v v 自己)都不能使它们不连通,我们就说 u u u v v v 点双连通。
  • 每个割点,至少属于两个联通分量

模板

  • tarjan找桥: 将图缩成一个边只有桥的树
    • dfn[N]:第一次 dfs 到的时间
    • low[N]:能到达的最早时间
    • st[N]:该边是否为桥
    • from:该点是从哪条边过来的
    • 关于^:由于存边时候存的是双向边,且正向边和反向边是连续的,从下标 2 开始存,那么一条条无向边是这样存的:{2,3},{4,5}…。而 ^ 有如下性质:如果是一个偶数 ^ 1,那么答案是偶数+1.如果是一个奇数 ^ 1,那么答案是奇数-1。第一次调用 tarjan from 传入一个负数即可。所以一条边 ^1 表示其反向边。
    • 算法流程:
      如果 low[j]>dfn[u] 表示该点连接的下面那个点不能通过其他边连接到当前点,故该边为桥
      在这里插入图片描述
void tarjan(int u,int from){
    dfn[u]=low[u]=++timestamp;
    stk.push(u);
    
    for(int i=head[u];i;i=nxt[i]){
        int j=to[i];
        
        if(!dfn[j]){
            tarjan(j,i);
            low[u]=min(low[u],low[j]);
            if(low[j]>dfn[u])
                st[i]=st[i^1]=true;
        }
        else if(i!=(from^1))    low[u]=min(low[u],dfn[j]);
    }
    
    if(dfn[u]==low[u]){
        int t;
        dcc_cnt++;
        do{
            t=stk.top();
            stk.pop();
            id[t]=dcc_cnt;
        }while(t!=u);
    }
}
  • tarjan找割点和缩成一个个含割点的双连通分量
    • vector<int>dcc[N]:存双连通分量的点
    • cut[N]:是否为割点
    • 注意存分量的方式和之前都不同,逻辑和上面差不多,多一个等号
void tarjan(int u){
    dfn[u]=low[u]=++timestamp;
    stk.push(u);
    
    if(u==root&&!head[u]){
        dcc[++dcc_cnt].push_back(u);
        return;
    }
    
    int cnt=0;
    for(int i=head[u];i;i=nxt[i]){
        int j=to[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();
                    stk.pop();
                    dcc[dcc_cnt].push_back(y);
                }while(y!=j);
                dcc[dcc_cnt].push_back(u);
            }
        }
        else    low[u]=min(low[u],dfn[j]);
    }
}

Acwing.395 冗余路径

  • 题意: 给定 F ( 5000 ) F(5000) F(5000) 个点和 R ( 10000 ) R(10000) R(10000) 条无向边,问至少要加多少条边才能使两个点之间有至少两条路可以互通
  • 思路: 使用 tarjan 将图所成一棵树,其中所有边都为桥,统计每个点的入度,如果只有 1 1 1 ,那么说明只能由一条边到达,cnt++即可
  • C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int head[N],idx=1,to[N],nxt[N];
int dcc_cnt,timestamp,dfn[N],low[N],id[N];
stack<int>stk;
int n,m,d[N];
bool st[N];

void add(int u,int v){
    to[++idx]=v,nxt[idx]=head[u],head[u]=idx;
}

void tarjan(int u,int from){
    dfn[u]=low[u]=++timestamp;
    stk.push(u);
    
    for(int i=head[u];i;i=nxt[i]){
        int j=to[i];
        
        if(!dfn[j]){
            tarjan(j,i);
            low[u]=min(low[u],low[j]);
            if(low[j]>dfn[u])
                st[i]=st[i^1]=true;
        }
        else if(i!=(from^1))    low[u]=min(low[u],dfn[j]);
    }
    
    if(dfn[u]==low[u]){
        int t;
        dcc_cnt++;
        do{
            t=stk.top();
            stk.pop();
            id[t]=dcc_cnt;
        }while(t!=u);
    }
}

int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int a,b;
        cin>>a>>b;
        add(a,b),add(b,a);
    }
    
    tarjan(1,10000);
    
    for(int i=2;i<=idx;i++)
        if(st[i])
            d[id[to[i]]]++;
    
    int cnt=0;
    
    for(int i=1;i<=dcc_cnt;i++){
        if(d[i]==1)
            cnt++;
    }
    
    cout<<(cnt+1)/2;
    
    
    return 0;
}

Acwing.1183 电力

  • 题意: 给定一个由 n n n 个点 m m m 条边构成的无向图,请你求出该图删除一个点之后,连通块最多有多少。
  • 思路: 可能有多个连通块,故需要统计一开始的连通块个数,然后用 tarjan 求出每个联通块里面对于每个割点的连通块个数,主要要特判是否为 root,如果不是 root 则计数加一
  • C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int head[N],to[N],idx,nxt[N];
int timestamp,dfn[N],low[N];
int n,m,root,ans;

void add(int u,int v){
    to[++idx]=v,nxt[idx]=head[u],head[u]=idx;
}

void tarjan(int u){
    dfn[u]=low[u]=++timestamp;
    
    int cnt=0;
    
    for(int i=head[u];i;i=nxt[i]){
        int j=to[i];
        
        if(!dfn[j]){
            tarjan(j);
            low[u]=min(low[u],low[j]);
            if(dfn[u]<=low[j])  cnt++;
        }
        else    low[u]=min(low[u],dfn[j]);
    }
    
    if(u!=root) cnt++;
    
    ans=max(ans,cnt);
}

int main(){
    while(cin>>n>>m,n||m){
        memset(head,0,sizeof head);
        memset(dfn,0,sizeof dfn);
        idx=0;
        timestamp=0;
        ans=0;
        
        for(int i=1;i<=m;i++){
            int a,b;
            cin>>a>>b;
            add(a,b),add(b,a);
        }
        
        int cnt=0;
        for(root=0;root<n;root++){
            if(!dfn[root]){
                cnt++;
                tarjan(root);
            }
        }
        
        cout<<ans+cnt-1<<endl;        
    }
}

Acwing.396 矿场搭建

  • 题意: 给定一个无向图(矿场),现要设立一些安全点,使得图中任意某个点被删去时,其他点都能到达一个安全点,求最少需要设置几个安全点,以及不同最少安全点的方案总数
  • 思路: 先使用 tarjan 把图缩成一个个双连通分量和一个个割点。
    • 首先对于整个图,最少需要两个安全点,因为只有一个安全点可能被删去
    • 再分别看每个连通块:①如果无割点,那么两个安全点随便设置,cnt+=2,方案数 C 缩点后该连通块的点数 2 C_{缩点后该连通块的点数}^{2} C缩点后该连通块的点数2。②如果有割点的话,那么由于割点的特性:删掉一个割点就会分为很多块,那么对于每一个双连通分量,如果其度数为1,那么就需要在这里放一个安全点,如果大于一,说明可以通过其他路走掉,无需放置安全点。
  • C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int head[N],to[N],nxt[N],idx;
int dcc_cnt,dfn[N],low[N],timestamp;
bool cut[N];
vector<int>dcc[N];
stack<int>stk;
int root,n,m,T;

void add(int u,int v){
    to[++idx]=v,nxt[idx]=head[u],head[u]=idx;
}

void tarjan(int u){
    dfn[u]=low[u]=++timestamp;
    stk.push(u);
    
    if(u==root&&!head[u]){
        dcc[++dcc_cnt].push_back(u);
        return;
    }
    
    int cnt=0;
    for(int i=head[u];i;i=nxt[i]){
        int j=to[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();
                    stk.pop();
                    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(){
    while(cin>>m,m){
        for(int i=1;i<=dcc_cnt;i++) dcc[i].clear();
        dcc_cnt=0,idx=0,timestamp=0,n=0;
        while(stk.size())   stk.pop();
        memset(head,0,sizeof head);
        memset(dfn,0,sizeof dfn);
        memset(cut,0,sizeof cut);
        
        while(m--){
            int a,b;
            cin>>a>>b;
            n=max(a,n);
            n=max(b,n);
            add(a,b),add(b,a);
        }
        
        for(root=1;root<n;root++)
            if(!dfn[root])
                tarjan(root);
                
        int res=0;
        unsigned long long 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   res ++, num *= dcc[i].size() - 1;
        }

        printf("Case %d: %d %llu\n", ++T , res, num);
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要获取无向图双连通分量,可以使用Tarjan算法。以下是一个Python实现的示例代码: ``` def tarjan_biconnected_components(graph): """ Tarjan算法计算无向图双连通分量 :param graph: 无向图,用邻接列表表示 :return: 双连通分量列表 """ index_counter = [0] stack = [] lowlink = {} index = {} result = [] bridges = [] def strongconnect(node): # 为节点赋予唯一的索引 index[node] = index_counter[0] lowlink[node] = index_counter[0] index_counter[0] += 1 stack.append(node) # 对于每个相邻节点v for v in graph[node]: # 如果v没有被访问过,则递归调用strongconnect if v not in index: strongconnect(v) lowlink[node] = min(lowlink[node], lowlink[v]) # 如果v是一个桥,则将桥添加到bridges列表中 if lowlink[v] == index[v]: bridges.append((node, v)) # 如果v已经在堆栈中,则更新此节点的lowlink elif v in stack: lowlink[node] = min(lowlink[node], index[v]) # 如果节点是一个连接分量的根,则弹出堆栈,并收集连通分量 if lowlink[node] == index[node]: connected_component = [] while True: v = stack.pop() connected_component.append(v) if v == node: break result.append(connected_component) for node in graph: if node not in index: strongconnect(node) return result ``` 使用示例: ``` graph = { 1: {2, 3}, 2: {1, 3, 4}, 3: {1, 2, 4}, 4: {2, 3, 5}, 5: {4} } result = tarjan_biconnected_components(graph) print(result) # 输出:[[1, 2, 3], [4, 5]] ``` 以上代码实现了Tarjan算法,用于计算无向图双连通分量。传入的图以邻接列表表示,返回的结果是双连通分量的列表。对于结果中的每个双连通分量,其包含的节点组成了一个强连通分量,即任意两个节点都有一条路径相连。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值