桥:如果删除某条边图不联通,称该边为桥。
割点:如果删除某点及其附属边后整个图不连通,成该点为割点。
两类双连通分量
边双连通分量(e_DCC):极大的不含桥的连通块。
删除边双连通分量中的任意边,整个图还是连通的
边双连通分量任意两点之间必有不少于两条不同路径
点双连通分量(v_DCC)/连通块:极大的不包含割点的连通块。
每一个割点至少属于两个点连通分量。

两个割点之间的边不一定是桥。例如一个H上边封闭的图。 桥两边的端点也不一定是割点。例如一条线段。

点连通分量和双连通分量也无必然关系 例如,一个8表示的图是边连通分量,不是点连通分量 一条线表示的图不是边连通分量,是点连通分量

对于一棵树来讲,每个节点都是一个边连通分量,每条边都是一个点连通分量

边连通分量

类似于强连通分量dfn, low数组
只存在前向边,树枝边,后向边,不存在横向边(因为边是没有方向的)

如何找到所有桥?
桥等价于对于一条树枝边x-ydfn[x] < low[y]
如何找到所有边双连通分量?
方法1:删掉所有桥
方法2:类似于双连通分量的操作,使用栈辅助操作

AcWing 395. 冗余路径
边双连通分量等价于图上任意两点之间至少有两条完全不同的路径
证明:
充分性:反证法,假设图不是一个边双连通分量,说明存在桥,删除桥后,两侧节点之间没有可达路径,与已知矛盾,得证
必要性:归纳法
无向连通图,至少加多少条边能变成边双连通分量?
缩点后度为1的节点数(c + 1) // 2
连线方式:两个叶节点连起来与根节点成环。

  1. import java.util.*;
  2. public class Main {
  3. static final int N = 5010, M = 20010;
  4. static int[] h = new int[N], e = new int[M], ne = new int[M];
  5. static int idx, p;
  6. static int[] dfn = new int[N], low = new int[N], stk = new int[N];
  7. static int dcc_cnt, time_cnt;
  8. static int n, m;
  9. static int[] id = new int[N];
  10. static boolean[] is_bridge = new boolean[M];
  11. static int[] degree = new int[N];
  12. public static void main(String[] args) {
  13. Scanner sc = new Scanner(System.in);
  14. n = sc.nextInt();
  15. m = sc.nextInt();
  16. Arrays.fill(h, -1);
  17. for (int i = 0; i < m; i++) {
  18. int a = sc.nextInt();
  19. int b = sc.nextInt();
  20. add(a, b);
  21. add(b, a);
  22. }
  23. tarjan(1, -1);
  24. for (int i = 0; i < idx; i++) {
  25. if (is_bridge[i]) {
  26. degree[id[e[i]]]++;
  27. }
  28. }
  29. int c = 0;
  30. for (int i = 1; i <= dcc_cnt; i++)
  31. if (degree[i] == 1)
  32. c++;
  33. System.out.println((c + 1) / 2);
  34. }
  35. static void tarjan(int u, int from) {
  36. dfn[u] = low[u] = ++time_cnt;
  37. stk[++p] = u;
  38. for (int i = h[u]; i != -1; i = ne[i]) {
  39. int j = e[i];
  40. if (dfn[j] == 0) {
  41. tarjan(j, i);
  42. low[u] = Math.min(low[u], low[j]);
  43. if (dfn[u] < low[j]) {
  44. is_bridge[i] = is_bridge[i ^ 1] = true;
  45. }
  46. } else if ((i ^ 1) != from) {
  47. low[u] = Math.min(low[u], dfn[j]);
  48. }
  49. }
  50. if (dfn[u] == low[u]) {
  51. int y;
  52. dcc_cnt++;
  53. do {
  54. y = stk[p--];
  55. id[y] = dcc_cnt;
  56. } while (y != u);
  57. }
  58. }
  59. static void add(int a, int b) {
  60. e[idx] = b;
  61. ne[idx] = h[a];
  62. h[a] = idx++;
  63. }
  64. }

点双连通分量

类似于强连通分量dfn, low数组

如何判断一个点是不是割点?
对于一条树枝边x-y,若low[y] >= dfn[x]

  • 如果x不是根节点,那么x是割点
  • x是根节点,至少有两个子节点yi,满足low[yi] >= dfn[x]

AcWing 1183. 电力
求每个割点最多能将图分成几个连通块

如何求点双连通分量?

  1. // 求完tarjan(j)后
  2. if (dfn[u] <= low[j]) {
  3. cnt++;
  4. if (u != root || cnt > 1) // 说明x是割点
  5. {
  6. // 记录该点为割点
  7. }
  8. // 将栈中元素弹出,直至j被弹出为止
  9. // 将u加入该点双连通分量中
  10. }
  11. // 最后判断u是不是孤立点

AcWing 396. 矿场搭建
分别处理每个连通块
若连通块内无割点,任选两个点作为两个出口即可,cnt * (cnt - 1) / 2
若连通块内有割点,先缩点

  • 每个割点作为单独一个点
  • 从每个v-DCC向其所包含的每个割点连一条边,这样缩点后的图最多有2倍的原点数量
    • v-DCC度为1(相当于叶节点),需要在该连通分量内部(非割点处)放一个出口 cnt - 1
    • v-DCC度> 1,无需设置出口