Tarjan 算法是由 Robert Tarjan 提出的,可以求解图中的强连通分量的算法。
Tarjan 求强连通分量
如果一个有向图是强连通图,那么它的任意一对结点都可以相互到达。一个有向图的某个极大的强连通子图称为强连通分量。不难发现所有强连通分量不会有重叠。
Tarjan 算法是用来求解图中强连通分量的算法。我们定义 $\text{dfn}(i)$ 表示 $i$ 点在 DFS 树上的 DFS 序,$\text{low}(i)$ 表示 $i$ 点向下走能够访问到的结点中 dfn 的最小值。
对于每一个结点 $u$,我们访问与它相邻的点 $v$。如果这个点没被访问过,那我们就访问这个点,并用 $\text{low}(v)$ 来更新 $\text{low}(u)$。如果这个点被访问过了,我们需要看它是否已经被算进某个强连通分量中。只有它没有被算进去,我们才可以用 $\text{dfn}(v)$ 来更新 $\text{low}(u)$。因为我们要判断某个点是否已经被算进某个强连通分量中,所以我们要维护一个 bool 类型的数组 in。当 $\text{dfn}(u) = \text{low}(u)$ 的时候,我们发现从 $u$ 出发不能访问到比 $u$ 更靠前的结点了,也就是产生了一个以 $u$ 为根的强连通分量。
在 DFS 的过程中,我们维护一个栈,每次 DFS 到一个结点,我们往栈中加入这个结点,并把这个结点的 in 设成 true
。拓展完所有与该结点相邻的结点后,我们判断该结点是否为一个强连通分量的根。如果是的话,该强连通分量一定是栈顶开始的一个前缀。于是我们暴力弹栈,然后将元素的 in 设为 false
即可。
下面的代码对应的题目是「BZOJ 2438」杀人游戏。这里是本题的题解。
1 |
|
2 |
|
3 |
|
4 | using namespace std; |
5 | |
6 | const int maxn = 1e5, maxm = 3e5; |
7 | int n, m, u[maxm + 3], v[maxm + 3], tm, dfn[maxn + 3], low[maxn + 3]; |
8 | int top, st[maxn + 3], cnt, sum[maxn + 3], bel[maxn + 3], deg[maxn + 3]; |
9 | bool in[maxn + 3], ok[maxn + 3]; |
10 | vector<int> G[maxn + 3]; |
11 | |
12 | void add(int u, int v) { |
13 | G[u].push_back(v); |
14 | } |
15 | |
16 | void tarjan(int u) { |
17 | dfn[u] = low[u] = ++tm; |
18 | st[++top] = u, in[u] = true; |
19 | for (int i = 0, v; i < G[u].size(); i++) { |
20 | v = G[u][i]; |
21 | if (!dfn[v]) { |
22 | tarjan(v); |
23 | low[u] = min(low[u], low[v]); |
24 | } else if (in[v]) { |
25 | low[u] = min(low[u], dfn[v]); |
26 | } |
27 | } |
28 | if (dfn[u] == low[u]) { |
29 | ++cnt; |
30 | do { |
31 | sum[cnt]++; |
32 | bel[st[top]] = cnt; |
33 | in[st[top]] = false; |
34 | } while (st[top--] != u); |
35 | } |
36 | } |
37 | |
38 | int main() { |
39 | scanf("%d %d", &n, &m); |
40 | for (int i = 1; i <= m; i++) { |
41 | scanf("%d %d", &u[i], &v[i]); |
42 | add(u[i], v[i]); |
43 | } |
44 | for (int i = 1; i <= n; i++) { |
45 | if (!dfn[i]) { |
46 | tarjan(i); |
47 | } |
48 | } |
49 | for (int i = 1; i <= n; i++) { |
50 | G[i].clear(); |
51 | } |
52 | for (int i = 1; i <= m; i++) { |
53 | if (bel[u[i]] != bel[v[i]]) { |
54 | deg[bel[v[i]]]++; |
55 | G[bel[u[i]]].push_back(bel[v[i]]); |
56 | } |
57 | } |
58 | int ans = 0; |
59 | for (int i = 1; i <= cnt; i++) { |
60 | ans += !deg[i]; |
61 | } |
62 | for (int i = 1; i <= cnt; i++) { |
63 | if (!deg[i] && sum[i] == 1) { |
64 | bool flag = true; |
65 | for (int j = 0; j < G[i].size(); j++) { |
66 | if (deg[G[i][j]] == 1) { |
67 | flag = false; |
68 | break; |
69 | } |
70 | } |
71 | if (flag) { |
72 | ans--; |
73 | break; |
74 | } |
75 | } |
76 | } |
77 | printf("%.6lf\n", 1. * (n - ans) / n); |
78 | return 0; |
79 | } |
Tarjan 求割点
在一张无向图上,一个结点是割点,当且仅当删除掉这个结点以及与之相邻的所有边后,图变得不再联通。
Tarjan 算法可以推广到无向图上来求割点。我们进行 DFS,按照有向图上 Tarjan 的方法求出 dfn 数组和 low 数组。这里 low 数组的定义稍有改变,$\text{low}(i)$ 表示点 $i$ 不经过它的父亲结点,能够绕到的结点最小 dfn。为了方便起见,对于每条边 $(u, v)$,我们会将 $\text{low}(v) \leftarrow \min { \text{low}(v), \text{dfn}(u) }$。那么如果一个非根的结点 $u$ 是割点,那么一定存在 $\text{low(v)} = \text{dfn}(u)$。注意特判根结点是否是割点,如果是它在 DFS 树上就一定有大于等于 $2$ 个孩子。
注意我们在找到访问过的点的时候,它是一定在当前结点到根的路径上的,所以我们不需要维护 in 数组。并且因为我们最后会进行 $\text{low}(v) \leftarrow \min { \text{low}(v), \text{dfn}(u) }$ 的操作,那么我们在如果访问到了一个访问过的结点那么就不用特判它是否为原结点的父亲了,我们就可以直接来用 $\text{dfn}(v)$ 来更新 $\text{low}(u)$。
下面代码对应的题目是:「模版」割点(Luogu 3388)。1
2
3
using namespace std;
4
5
const int maxn = 1e6, maxm = maxn * 2;
6
int n, m, tm, dfn[maxn + 3], low[maxn + 3];
7
int tot, ter[maxm + 3], nxt[maxm + 3], lnk[maxn + 3];
8
bool cut[maxn + 3];
9
10
void add(int u, int v) {
11
ter[++tot] = v, nxt[tot] = lnk[u], lnk[u] = tot;
12
}
13
14
void tarjan(int u, bool is_rt = true) {
15
dfn[u] = low[u] = ++tm;
16
int cnt = 0;
17
for (int i = lnk[u], v; i; i = nxt[i]) {
18
v = ter[i];
19
if (!dfn[v]) {
20
tarjan(v, false);
21
low[u] = min(low[u], low[v]);
22
if (!is_rt && low[v] == dfn[u]) {
23
cut[u] = true;
24
}
25
cnt++;
26
} else {
27
low[u] = min(low[u], dfn[v]);
28
}
29
}
30
if (is_rt && cnt >= 2) {
31
cut[u] = true;
32
}
33
}
34
35
int main() {
36
scanf("%d %d", &n, &m);
37
for (int i = 1, u, v; i <= m; i++) {
38
scanf("%d %d", &u, &v);
39
add(u, v), add(v, u);
40
}
41
for (int i = 1; i <= n; i++) if (!dfn[i]) {
42
tarjan(i);
43
}
44
int res = 0;
45
for (int i = 1; i <= n; i++) {
46
res += cut[i];
47
}
48
printf("%d\n", res);
49
for (int i = 1; i <= n; i++) {
50
if (cut[i]) {
51
printf("%d%c", i, " \n"[!--res]);
52
}
53
}
54
return 0;
55
}
Tarjan 求桥
桥是无向图上的某条边,满足删去这条边以后图变得不联通。
桥的求法和割点类似,我们有相同的 dfn 数组和 low 数组的定义。只是我们不再有 “为了方便起见” 对定义的略微修改。那么,如果一个结点到它的父亲的那条边是桥,那么有 $\text{dfn}(u) = \text{low}(u)$。和求割点类似,我们也不需要维护 in 数组。但是如果需要缩点的话我们还是需要维护一个栈。
求桥的难点在于处理重边。对于每一个结点,我们要记录到达每个结点的边的编号 $\text{id}(u)$。在找到一个访问过的结点的时候,我们就要判断到达该结点的边是否是 $\text{id}(u)$。如果不是的话,才使用 $\text{dfn}(v)$ 来更新 $\text{low}(u)$。
下面代码对应的题目是:「BZOJ 1718」Redundant Paths。这里是本题的题解。
1 |
|
2 |
|
3 | using namespace std; |
4 | |
5 | const int maxn = 1e6, maxm = maxn * 2; |
6 | int n, m, u[maxm + 3], v[maxm + 3]; |
7 | int tm, dfn[maxn + 3], low[maxn + 3], top, st[maxn + 3]; |
8 | int cnt, bel[maxn + 3], deg[maxn + 3], cur; |
9 | int tot, ter[maxm + 3], nxt[maxm + 3], lnk[maxn + 3]; |
10 | bool vis[maxn + 3]; |
11 | |
12 | void add(int u, int v) { |
13 | ter[++tot] = v, nxt[tot] = lnk[u], lnk[u] = tot; |
14 | } |
15 | |
16 | int adj(int x) { |
17 | return x & 1 ? x + 1 : x - 1; |
18 | } |
19 | |
20 | void tarjan(int u, int e = 0) { |
21 | dfn[u] = low[u] = ++tm; |
22 | st[++top] = u; |
23 | for (int i = lnk[u], v; i; i = nxt[i]) { |
24 | v = ter[i]; |
25 | if (!dfn[v]) { |
26 | tarjan(v, i); |
27 | low[u] = min(low[u], low[v]); |
28 | } else if (i != adj(e)) { |
29 | low[u] = min(low[u], dfn[v]); |
30 | } |
31 | } |
32 | if (dfn[u] == low[u]) { |
33 | cnt++; |
34 | do { |
35 | bel[st[top]] = cnt; |
36 | } while (st[top--] != u); |
37 | } |
38 | } |
39 | |
40 | void dfs(int u, int pa = 0) { |
41 | if (pa) { |
42 | cur -= (deg[u] == 1) + (deg[pa] == 1); |
43 | deg[u]++, deg[pa]++; |
44 | cur += (deg[u] == 1) + (deg[pa] == 1); |
45 | } |
46 | vis[u] = true; |
47 | for (int i = lnk[u], v; i; i = nxt[i]) { |
48 | v = ter[i]; |
49 | if (!vis[v]) { |
50 | dfs(v, u); |
51 | } |
52 | } |
53 | } |
54 | |
55 | int main() { |
56 | scanf("%d %d", &n, &m); |
57 | for (int i = 1; i <= m; i++) { |
58 | scanf("%d %d", &u[i], &v[i]); |
59 | add(u[i], v[i]), add(v[i], u[i]); |
60 | } |
61 | for (int i = 1; i <= n; i++) { |
62 | if (!dfn[i]) tarjan(i); |
63 | } |
64 | tot = 0; |
65 | for (int i = 1; i <= n; i++) { |
66 | lnk[i] = 0; |
67 | } |
68 | for (int i = 1; i <= m; i++) if (bel[u[i]] != bel[v[i]]) { |
69 | add(bel[u[i]], bel[v[i]]), add(bel[v[i]], bel[u[i]]); |
70 | } |
71 | int ans = 0; |
72 | for (int i = 1; i <= cnt; i++) if (!vis[i]) { |
73 | cur = 0, dfs(i); |
74 | ans += (cur + 1) / 2; |
75 | } |
76 | printf("%d\n", ans); |
77 | return 0; |
78 | } |