双连通分量回顾与总结

本文详细介绍了图论中的割点、割边概念,以及点双连通分量和边双连通分量的定义。通过给出的模板代码,展示了如何在O(n)时间内求解割点、割边,并探讨了它们在图的连通性中的作用。同时,文章还讨论了点双连通分量和边双连通分量的特性,如点双连通分量中不存在割点,以及边双连通分量与桥的关系。
摘要由CSDN通过智能技术生成

割点

  • 在无向图中,所有能互通的点构成了一个连通分量,在这些连通分量中有一些关键点,如果删除它,会把这个连通分量分成两个或者更多,这种点被称为割点
  • 如下图的节点 3 , 4 , 5 3,4,5 3,4,5均为割点
    在这里插入图片描述
  • 定理1:T的根节点s是割点,当且仅当s有两个或更多的子节点
  • 定理2:T的非根节点u是割点,当且仅当u存在一个子节点v,v及其后代都没有回退边连回u的祖先
  • 利用这样两个定理,我们可以在 O ( n ) O(n) O(n)的时间复杂度内求出一个图的所有割点
  • 和SCC的求法相似的。设 n u m [ i ] num[i] num[i]表示 D f s Dfs Dfs对每个点的访问顺序, l o w [ i ] low[i] low[i]表示 i i i能够连到祖先的 n u m num num
  • 如果最后发现 l o w [ v ] ≥ n u m [ u ] low[v]\geq num[u] low[v]num[u],那就说明父亲节点 u u u进入递归的顺序总是小于等于子节点 v v v回退的位置,那就说明 u u u是一个割点
    模板
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int n, m;
  cin >> n >> m;
  vector<vector<int> > g(n + 1);
  for(int i=0;i<m;i++){
    int u, v;
    cin >> u >> v;
    g[u].push_back(v);
    g[v].push_back(u);
  }
  vector<int> num(n + 1), low(n + 1);
  vector<bool> iscut(n + 1);
  int dfn = 0;
  function<void(int, int)> Dfs = [&](int u, int fa){
    low[u] = num[u] = ++dfn;
    int child = 0;
    for(auto v : g[u]){
      if(!num[v]){
        child += 1;
        Dfs(v, u);
        low[u] = min(low[u], low[v]);
        if(low[v] >= num[u] && u != fa){
          iscut[u] = true;
        }
      }else if(num[v] < num[u] && v != fa){//处理回退边,表示到达了祖先节点
        low[u] = min(low[u], num[v]);
      }
    }
    if(u == fa && child >= 2){//判断根节点是不是割点
      iscut[u] = true;
    }
  };
  for(int i=1;i<=n;i++){
    if(!num[i]){
      Dfs(i, i);
    }
  }
  int tot = 0;
  for(int i=1;i<=n;i++){
    if(iscut[i]) tot += 1;
  }
  cout << tot << '\n';
  for(int i=1;i<=n;i++){
    if(iscut[i]){
      cout << i << ' ';
    }
  }
  return 0;
}

割边

  • 同理,在一个连通分量中,如果删除一条边,把这个连通分量分成了两个,那么这条边就是割边
  • 类似的,如果子节点的回退位置在父亲节点进入递归之后(不相等),那么说明它和父亲节点之间的连边就是割边
    模板
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int n, m;
  cin >> n >> m;
  vector<vector<int> > g(n + 1);
  for(int i=0;i<m;i++){
    int u, v;
    cin >> u >> v;
    g[u].push_back(v);
    g[v].push_back(u);
  }
  vector<int> low(n + 1), num(n + 1);
  int dfn = 0;
  int ans = 0;
  function<void(int, int)> Dfs = [&](int u, int fa){
    low[u] = num[u] = ++dfn;
    for(auto v : g[u]){
      if(!num[v]){
        Dfs(v, u);
        low[u] = min(low[u], low[v]);
        if(low[v] > num[u]){//子节点回退的位置大于父亲节点进入Dfs的位置,说明u->v是割边
          ans += 1;
        }
      }else if(v ^ fa){//如果v != fa
        low[u] = min(low[u], num[v]);
      }
    }
  };
  for(int i=1;i<=n;i++){
    if(!num[i]){
      Dfs(i, i);
    }
  }
  cout << ans;
  return 0;
}

确认模板

  • 打印具体割边
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int main(){
  int n;
  int tt;
  scanf("%d", &tt);
  for(int kase=1;kase<=tt;kase++){
    scanf("%d", &n);
    vector<vector<int> > g(n + 1);
    for(int i=0;i<n;i++){
      int u, v, m;
      scanf("%d (%d)", &u, &m);
      for(int j=0;j<m;j++){
        scanf("%d", &v);
        g[u].push_back(v);
      }
    }
    vector<int> low(n + 1), num(n + 1);
    int dfn = 0;
    int ans = 0;
    vector<pair<int, int> > vs;
    function<void(int, int)> Dfs = [&](int u, int fa){
      low[u] = num[u] = ++dfn;
      for(auto v : g[u]){
        if(!num[v]){
          Dfs(v, u);
          low[u] = min(low[u], low[v]);
          if(low[v] > num[u]){//子节点回退的位置大于父亲节点进入Dfs的位置,说明u->v是割边
            vs.push_back(make_pair(min(u, v), max(u, v)));
            ans += 1;
          }
        }else if(v ^ fa){//如果v != fa
          low[u] = min(low[u], num[v]);
        }
      }
    };
    for(int i=1;i<=n;i++){
      if(!num[i]){
        Dfs(i, i);
      }
    }
    printf("Case %d:\n", kase);
    printf("%d critical links\n", ans);
    sort(vs.begin(), vs.end());
    for(auto i : vs){
      printf("%d - %d\n", i.first, i.second);
    }
  }
  return 0;
}

点双连通分量

  • 在一个连通图中选任意两个点,如果他们之间至少存在两条点不重复的路径,称为点双连通,换句话说,无论删除除了这两个点之外的其余哪一个点,这两个点仍然能够找到通路,这两个点就是点双连通的
  • 一个图中的极大连通子图称为点双连通分量,在一个点双连通分量中去掉任意一个点,其他点仍然是连通的
  • 也就是说,点双连通分量中没有割点
    http://poj.org/problem?id=1523
  • 求一个图中有多少个点删除之后会产生新的连通分量,并输出产生的连通分量,代码如下
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>
#include <map>
#include <iomanip>

using namespace std;

const int N = 2e3 + 100;
struct Edge{
  int next;
  int to;
  int val;
}edge[N];
int head[N], low[N], num[N];
int cnt = 0;
void Add_Edge(int u, int v, int w){
  edge[cnt].next = head[u];
  edge[cnt].to = v;
  edge[cnt].val = w;
  head[u] = cnt++;
}
int subnets[N];
bool f = false;
int dfn = 0;
int n;
void tarjan(int u, int fa){
  low[u] = num[u] = ++dfn;
  int child = 0;
  for(int i=head[u];~i;i=edge[i].next){
    int v = edge[i].to;
    if(!num[v]){
      child += 1;
      tarjan(v, u);
      low[u] = min(low[u], low[v]);
      if((u == fa && child > 1) || (low[v] >= num[u] && u != fa)){
        f = true;
        subnets[u] += 1;
        //u->v  该边导致u成为割点 
       	//当dfn[u]==low[v]时u->v为返祖边,u、v处于同一双连通分量中 
        //当dfn[u]<low[v]时u->v为割边
        //删除割点u产生的连通数目为:u所在的连通分量数目+与u所连接的割边的数目+1(边:fa->u) 
      }
    }else if(v != fa){
      low[u] = min(low[u], num[v]);
    }
  }
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int u, v;
  int kase = 1;
  while(true){
    cnt = dfn = 0;
    memset(head, -1, sizeof head);
    memset(num, 0, sizeof num);
    memset(low, 0, sizeof low);
    memset(subnets, 0, sizeof subnets);
    f = false;
    n = -1;
    while(cin >> u && u){
      cin >> v;
      Add_Edge(u, v, 1);
      Add_Edge(v, u, 1);
      n = max(max(u, v), n);
    }
    if(n == -1) break; 
    for(int i=1;i<=n;i++){
      if(!num[i]){
        tarjan(i, i);
      }
    }
    cout << "Network #" << kase << '\n';
    if(!f) cout << "  No SPF nodes\n";
    else{
      for(int i=1;i<=n;i++){
        if(subnets[i] > 0){
          cout << "  SPF node " << i << " leaves " << subnets[i] + 1 << " subnets\n";           
        }
      }
    }
    kase += 1;
    cout << '\n';
  }
  return 0;
}

边双连通分量

  • 如果任意两点之间至少存在两条边不重复的路径,称为“边双连通”,也就是桥不属于任何边双连通分量,那么我们可以把所有桥删除掉,划分出的若干个连通分量就是若干个边双连通分量
  • 这里使用前向星存图,标记所有割边,然后再次 D f s Dfs Dfs的时候不走这些割边,给不同的连通分量编号即可进行相关运算
    模板题
  • 问所有边双连通分量各自的异或和
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

const int N = 1e6 + 100;
struct Edge{
  int next;
  int to;
  int val;
}edge[N];
int head[N], ans[N];
int cnt = 0;
void Add_Edge(int u, int v, int w){
  edge[cnt].next = head[u];
  edge[cnt].to = v;
  edge[cnt].val = w;
  head[u] = cnt++;
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int n, m;
  cin >> n >> m;
  vector<int> w(n + 1);
  memset(head, -1, sizeof head);
  for(int i=1;i<=n;i++){
    cin >> w[i];
  }
  for(int i=0;i<m;i++){
    int u, v;
    cin >> u >> v;
    Add_Edge(u, v, 1);
    Add_Edge(v, u, 1);
  }
  vector<int> low(n + 1), num(n + 1);
  vector<bool> bridge(cnt + 1);
  int dfn = 0;
  function<void(int, int)> tarjan = [&](int u, int fa){
    low[u] = num[u] = ++dfn;
    for(int i=head[u];~i;i=edge[i].next){
      int v = edge[i].to;
      if(v == fa) continue;
      if(!num[v]){
        tarjan(v, u);
        low[u] = min(low[v], low[u]);
        if(low[v] > num[u]){//标记割边
          bridge[i] = true;
          bridge[i ^ 1] = true;//前向星的边编号是相邻的两个奇数
        }
      }else if(v ^ fa){
        low[u] = min(low[u], num[v]);
      }
    }
  };
  for(int i=1;i<=n;i++){
    if(!num[i]){
      tarjan(i, i);
    }
  }
  vector<int> bccNo(n + 1);
  int bccCount = 0;
  function<void(int)> Dfs = [&](int u){
    bccNo[u] = bccCount;// SCC编号
    ans[bccCount] ^= w[u];
    for(int i=head[u];~i;i=edge[i].next){
      int v = edge[i].to;
      if(bridge[i]) continue;
      if(bccNo[v] == 0){
        Dfs(v);
      }
    }
  };
  for(int i=1;i<=n;i++){
    if(bccNo[i] == 0){
      bccCount += 1;
      Dfs(i);
    }
  }
  sort(ans + 1, ans + bccCount + 1);
  for(int i=1;i<=bccCount;i++) cout << ans[i] << '\n';
  return 0;
}

总结

  • 割点和割边应该来说是比较常见的问题,上次济南热身赛好像就有个割边的题,这些都是针对于无向图的,记不太清楚就把模板打好带着
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clarence Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值