强连通分量
基础概念
-
强连通图:任意两点间能互相到达的有向图称为 强连通图。
-
强连通分量:有向图的极大强连通子图称为 强连通分量,英文缩写为 SCC。
-
缩点:把强连通分量替换为点的操作称为 缩点,经过缩点之后的图是 DAG。
Tarjan 算法
DFS 生成树
以下为 DFS 生成树的示意图。
有向图的 DFS 生成树主要有 \(4\) 种边,但不一定 \(4\) 种全部出现。
-
树边:搜索到一个未访问过的点时形成一条 树边。示意图中以黑色边表示。
-
返祖边:终点是起点的祖先的边称为 返祖边。示意图中以红色边(即 \(7 \rightarrow 1\))表示。
-
前向边:起点是终点的祖先的边称为 前向边,搜索到子树中的结点形成一条前向边。示意图中以绿色边(即 \(3 \rightarrow 6\))表示。
-
横叉边:搜索到一个已访问过的结点,该点不是当前结点的祖先时形成一条 横叉边,也是起点和终点没有什么关系的额外边。示意图中以蓝色边(即 \(9 \rightarrow 7\))表示。
考察 DFS 生成树与强连通分量之间的关系。
若结点 \(u\) 是某个强连通分量在搜索树中遇到的第一个点,则该强联通分量的其余结点必定在以 \(u\) 为根的子树中。称 \(u\) 为该 强连通分量的根。
定义 \(dfn\) 与 \(low\)
-
\(dfn[u]\) 表示 DFS 中结点 \(u\) 的时间戳,即 \(u\) 被搜索的次序。
-
\(low[u]\) 表示在 \(u\) 的子树中能通过一条额外边回溯到的最早的已在栈中的结点,即从 \(u\) 能回溯到的最顶端的祖先的 \(dfn\) 值。
特别地,一个结点 \(u\) 总能访问到它自己,所以 \(low[u] \leq dfn[u]\)。
统计思路与维护
考察额外边对于搜索树结构的影响,发现以下两条性质。
-
横叉边起点的 DFS 序必定比终点的 DFS 序大。
-
前向边相当于树中 \(u\) 到 \(v\) 的链。
换句话说,横叉边、前向边对于求解强连通分量是无用的。
在搜索过程中,用栈来维护强连通分量,对于 \(u\) 及其相邻子结点 \(v\) 考虑 \(3\) 种情况。
-
\(v\) 未被访问:继续对 \(v\) 搜索。在回溯过程中,用 \(low[v]\) 更新 \(low[u]\)。
-
\(v\) 被访问过,已在栈中:此时该边为树边或返祖边,用 \(dfn[v]\) 更新 \(low[u]\)。
-
\(v\) 被访问过,已出栈:\(v\) 所在的强连通分量已被处理,不做操作。
\(low[u] = dfn[u] \Longleftrightarrow\) 点 \(u\) 是强连通分量的顶端
此时栈中 \(u\) 及其上方的点构成一个强连通分量,可以做缩点操作。
代码实现
void tarjan(int u) {
// SCC
dfn[u] = low[u] = ++tim;
stk[++top] = u; instk[u] = true;
for (int v : e[u]) {
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (instk[v]) low[u] = min(low[u], dfn[v]);
}
// 缩点
if (low[u] == dfn[u]) {
scc_cnt++;
while (top) {
int x = stk[top--];
instk[x] = false;
scc[x] = scc_cnt;
if (x == u) break;
}
}
}
例题
校园网
问题 \(1\) 的实质是求缩点后入度为 \(0\) 的点的个数。
问题 \(2\) 则是求最少添加几条边能使 DAG 变成 SCC。因为环中每个结点的入度和出度都不为 \(0\),显然是从出度为 \(0\) 的点向入度为 \(0\) 的点连边。答案则是入度为 \(0\) 的点和出度为 \(0\) 的点的个数的最大值。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n, dfn[128], low[128], scc[128], scc_cnt, tim, stk[128], top;
int in[128], out[128], ans1, ans2;
bool instk[128];
vector<int> e[128];
void tarjan(int u) {
dfn[u] = low[u] = ++tim;
stk[++top] = u; instk[u] = true;
for (int v : e[u]) {
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (instk[v]) low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
scc_cnt++;
while (top) {
int x = stk[top--];
instk[x] = false;
scc[x] = scc_cnt;
if (x == u) break;
}
}
}
int main() {
cin >> n;
for (int u = 1, v; u <= n; u++) {
cin >> v;
while (v) {
e[u].push_back(v);
cin >> v;
}
}
for (int i = 1; i <= n; i++) if (!dfn[i]) tarjan(i);
for (int u = 1; u <= n; u++)
for (int v : e[u])
if (scc[u] != scc[v])
out[scc[u]]++, in[scc[v]]++;
for (int i = 1; i <= scc_cnt; i++)
ans1 += (in[i] == 0), ans2 += (out[i] == 0);
if (scc_cnt == 1) cout << "1\n0";
else cout << ans1 << "\n" << max(ans1, ans2);
return 0;
}
抢掠计划
“可以经过同一点或边多次”说明该有向图存在环,自然想到将它缩点成 DAG。
由于指定了一个起点,又要使利益最大化,不难想到在 DAG 上跑单源最长路。这里使用 SPFA 来实现。
启示:在读入点权之前先跑一遍 Tarjan,再记录数据有利于代码实现的简单化。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 8;
int dfn[N], low[N], scc[N], tim, stk[N], top, scc_cnt;
int n, m, st, p, a[N], dis[N];
bool instk[N], inque[N], bar[N];
vector<int> e[N], g[N];
void Tarjan(int u) {
dfn[u] = low[u] = ++tim;
stk[++top] = u; instk[u] = true;
for (int v : e[u]) {
if (!dfn[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (instk[v]) low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
scc_cnt++;
while (top) {
int x = stk[top--];
scc[x] = scc_cnt;
instk[x] = false;
if (x == u) break;
}
}
}
void SPFA() {
st = scc[st]; queue<int> q;
q.push(st); inque[st] = true; dis[st] = a[st];
while (!q.empty()) {
int u = q.front(); q.pop(), inque[u] = false;
for (int v : g[u])
if (dis[v] < dis[u] + a[v]) {
dis[v] = dis[u] + a[v];
if (!inque[v]) inque[v] = true, q.push(v);
}
}
}
int main() {
cin >> n >> m;
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
e[u].push_back(v);
}
for (int i = 1; i <= n; i++) if (!dfn[i]) Tarjan(i);
for (int i = 1, x; i <= n; i++) {
cin >> x;
a[scc[i]] += x;
}
cin >> st >> p;
for (int i = 1, x; i <= p; i++) {
cin >> x;
bar[scc[x]] = true;
}
for (int i = 1; i <= n; i++)
for (int v : e[i])
if (scc[i] != scc[v])
g[scc[i]].push_back(scc[v]);
SPFA();
int ans = 0;
for (int i = 1; i <= scc_cnt; i++) if (bar[i]) ans = max(ans, dis[i]);
cout << ans;
return 0;
}
受欢迎的牛
对于图中的强连通分量来说,只要有一个是“明星”,该强连通分量中的所有点都是“明星”。
反过来思考,如果两个点都是“明星”,那么它们一定在同一个强连通分量中。
进一步考虑,所有“明星”一定在一个出度为 \(0\) 的强连通分量中。答案就是这个强连通分量的点数。
特别地,若图中有大于等于 \(2\) 个强连通分量,则它们无法到达对方,也就没有“明星”了。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 8;
int n, m, ans, sz[N], out[N], num;
int dfn[N], low[N], tim, scc_cnt, scc[N], stk[N], top;
bool instk[N];
vector<int> e[N];
void Tarjan(int u) {
dfn[u] = low[u] = ++tim;
stk[++top] = u; instk[u] = true;
for (int v : e[u]) {
if (!dfn[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (instk[v]) low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
scc_cnt++;
while (top) {
int x = stk[top--];
scc[x] = scc_cnt;
sz[scc_cnt]++;
instk[x] = false;
if (x == u) break;
}
}
}
int main() {
cin >> n >> m;
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
e[u].push_back(v);
}
for (int i = 1; i <= n; i++) if (!dfn[i]) Tarjan(i);
for (int u = 1; u <= n; u++)
for (int v : e[u])
if (scc[u] != scc[v])
out[scc[u]]++;
for (int i = 1; i <= scc_cnt; i++)
if (out[i] == 0) ans += sz[i], num++;
cout << (num == 1 ? ans : 0);
return 0;
}

浙公网安备 33010602011771号