连通分量:对于一个有向图,一个分量中任意两点u, v
,必然可以从u
走到v
,从v
走到u
强连通分量:极大连通分量。
应用:
将有向图通过缩点变为有向无环图(DAG),即拓扑图。然后再做其它操作。
求解:
DFS的顺序,将所有边分为四大类
- 树枝边(
x -> y
,x
是y
的父节点) - 前向边(
x -> y
,x
是y
的祖先节点) - 横向边(
x -> y
,y
是之前搜过的其它分支上的节点) - 后向边(
x -> y
,y
是x
的祖先节点)
判断当前节点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从小到大添加的,所以其逆序就是拓扑排序。
板子
import java.util.*;
public class Main {
static final int N = 10010, M = 50010;
static int[] h = new int[N], e = new int[M], ne = new int[M];
static int idx;
static int[] dfn = new int[N], low = new int[N];
static int time_cnt;
static int[] stk = new int[N];
static int p = -1;
static boolean[] in_stk = new boolean[N];
static int[] id = new int[N], size = new int[N];
static int scc_cnt;
static int n, m;
static int[] dout = new int[N];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
Arrays.fill(h, -1);
for (int i = 0; i < m; i++) {
int a = sc.nextInt(), b = sc.nextInt();
add(a, b);
}
for (int i = 1; i <= n; i++) {
if (dfn[i] == 0) {
tarjan(i);
}
}
for (int i = 1; i <= n; i++) {
for (int j = h[i]; j != -1; j = ne[j]) {
int k = e[j];
int a = id[i], b = id[k];
if (a != b) {
dout[a]++;
}
}
}
int cnt = 0, sum = 0;
for (int i = 1; i <= scc_cnt; i++) {
if (dout[i] == 0) {
cnt++;
sum += size[i];
if (cnt > 1) {
sum = 0;
break;
}
}
}
System.out.println(sum);
}
static void tarjan(int u) {
dfn[u] = low[u] = ++time_cnt;
stk[++p] = u;
in_stk[u] = true;
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (dfn[j] == 0)
{
tarjan(j);
low[u] = Math.min(low[u], low[j]);
}
else if (in_stk[j]) {
low[u] = Math.min(low[u], dfn[j]);
}
}
if (dfn[u] == low[u]) {
int y;
++scc_cnt;
do {
y = stk[p--];
in_stk[y] = false;
id[y] = scc_cnt;
size[scc_cnt]++;
} while (y != u);
}
}
static void add(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
}
应用
AcWing 367. 学校网络
结论:缩点后的拓扑图至少加几条边才能变成一个强连通分量?
若本身就是一个强连通分量,加0条边就可以
若本身不是,统计入度为0的缩点的个数c1
和出度为0的缩点的个数c2
,需要加的边数等于max(c1, c2)
AcWing 1175. 最大半连通子图
强连通分量 + DP
AcWing 368. 银河
同糖果,之前用差分约束 + spfa
这次用强连通分量解决
tarjan缩点 -> 建缩点图 -> 拓扑序处理最长路 -> 遍历求和