连通分量:对于一个有向图,一个分量中任意两点u, v,必然可以从u走到v,从v走到u
强连通分量:极大连通分量。

应用:

将有向图通过缩点变为有向无环图(DAG),即拓扑图。然后再做其它操作。

求解:

DFS的顺序,将所有边分为四大类

  • 树枝边(x -> yxy的父节点)
  • 前向边(x -> yxy的祖先节点)
  • 横向边(x -> yy是之前搜过的其它分支上的节点)
  • 后向边(x -> yyx的祖先节点)

判断当前节点x是否在某个强连通分量(SCC)中?
情况1:存在一条后向边指向x的祖先节点。
情况2:先走一条横向边,再走到x的祖先节点。
无论哪种情况,能成环的都是当前点的祖先节点。相当于一条后向边直至祖先节点。
如果当前点有一条横向边到达一个未在栈中的点,说明走到了死胡同。

Tarjan算法求强连通分量(SCC)
引入时间戳的概念(dfs过程中的自然数),对于每个点来说,记录两个时间戳
dfn[u]表示遍历到u节点的时间戳
low[u]表示从u开始走能遍历到的最小时间戳。
如果u是其所在的强连通分量的最高点,等价于dfn[u] == low[u]

缩点:
遍历每条边,找到两个端点所在的强连通分量,如果不是同一个SCC,说明这两个强连通分量之间有一条边。

Tarjan算法执行完后,按照强连通分量编号scc_count从大到小的顺序就是缩点后的图的拓扑序。
为什么呢?因为dfs求拓扑序(dfs遍历每一个点的所有临点,然后将当前点加入队列)的结果就是队列的逆序。而Tarjan添加每一个强连通分量就是按照dfs从小到大添加的,所以其逆序就是拓扑排序。

AcWing 1174. 受欢迎的牛

板子

  1. import java.util.*;
  2. public class Main {
  3. static final int N = 10010, M = 50010;
  4. static int[] h = new int[N], e = new int[M], ne = new int[M];
  5. static int idx;
  6. static int[] dfn = new int[N], low = new int[N];
  7. static int time_cnt;
  8. static int[] stk = new int[N];
  9. static int p = -1;
  10. static boolean[] in_stk = new boolean[N];
  11. static int[] id = new int[N], size = new int[N];
  12. static int scc_cnt;
  13. static int n, m;
  14. static int[] dout = new int[N];
  15. public static void main(String[] args) {
  16. Scanner sc = new Scanner(System.in);
  17. n = sc.nextInt();
  18. m = sc.nextInt();
  19. Arrays.fill(h, -1);
  20. for (int i = 0; i < m; i++) {
  21. int a = sc.nextInt(), b = sc.nextInt();
  22. add(a, b);
  23. }
  24. for (int i = 1; i <= n; i++) {
  25. if (dfn[i] == 0) {
  26. tarjan(i);
  27. }
  28. }
  29. for (int i = 1; i <= n; i++) {
  30. for (int j = h[i]; j != -1; j = ne[j]) {
  31. int k = e[j];
  32. int a = id[i], b = id[k];
  33. if (a != b) {
  34. dout[a]++;
  35. }
  36. }
  37. }
  38. int cnt = 0, sum = 0;
  39. for (int i = 1; i <= scc_cnt; i++) {
  40. if (dout[i] == 0) {
  41. cnt++;
  42. sum += size[i];
  43. if (cnt > 1) {
  44. sum = 0;
  45. break;
  46. }
  47. }
  48. }
  49. System.out.println(sum);
  50. }
  51. static void tarjan(int u) {
  52. dfn[u] = low[u] = ++time_cnt;
  53. stk[++p] = u;
  54. in_stk[u] = true;
  55. for (int i = h[u]; i != -1; i = ne[i]) {
  56. int j = e[i];
  57. if (dfn[j] == 0)
  58. {
  59. tarjan(j);
  60. low[u] = Math.min(low[u], low[j]);
  61. }
  62. else if (in_stk[j]) {
  63. low[u] = Math.min(low[u], dfn[j]);
  64. }
  65. }
  66. if (dfn[u] == low[u]) {
  67. int y;
  68. ++scc_cnt;
  69. do {
  70. y = stk[p--];
  71. in_stk[y] = false;
  72. id[y] = scc_cnt;
  73. size[scc_cnt]++;
  74. } while (y != u);
  75. }
  76. }
  77. static void add(int a, int b) {
  78. e[idx] = b;
  79. ne[idx] = h[a];
  80. h[a] = idx++;
  81. }
  82. }

应用

AcWing 367. 学校网络
结论:缩点后的拓扑图至少加几条边才能变成一个强连通分量?
若本身就是一个强连通分量,加0条边就可以
若本身不是,统计入度为0的缩点的个数c1和出度为0的缩点的个数c2,需要加的边数等于max(c1, c2)

AcWing 1175. 最大半连通子图
强连通分量 + DP

AcWing 368. 银河
糖果,之前用差分约束 + spfa
这次用强连通分量解决
tarjan缩点 -> 建缩点图 -> 拓扑序处理最长路 -> 遍历求和