📅  最后修改于: 2023-12-03 15:12:47.471000             🧑  作者: Mango
给出一张 $n$ 个点 $m$ 条边的有向图,求出该图的强连通分量个数和每个强连通分量中点的个数。
强连通分量是指一个有向图中的一个最大强连通子图,即任意两个点都能互相到达的子图。求解强连通分量可以使用 Tarjan 算法,时间复杂度为 $O(n+m)$。具体实现方法是通过 DFS 遍历图,对于每一个节点维护一个 dfn
值和一个 low
值,dfn
表示该节点被遍历到的时间戳,low
表示该节点能够到达的最小的 dfn
值。当 DFS 进入一个节点时,给该节点打上时间戳,同时将该节点入栈;在 DFS 退回该节点时,查看该节点能够到达的最小 dfn
值是否小于自身的 dfn
值,若是,则说明该节点所在的子图为一个强连通分量,将栈内该节点及其能够到达的节点全部弹出并标记为同一强连通分量。
求解每个强连通分量中的节点个数可以在 Tarjan 算法的过程中维护。在弹出节点时,记录当前强连通分量中已经弹出的节点个数,最终可以得到每个强连通分量中的节点个数。
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
const int MAXN = 1e5 + 5;
vector<int> G[MAXN]; // 图的邻接表表示
int dfn[MAXN], low[MAXN], scc[MAXN]; // dfn 和 low 分别表示节点的时间戳和最小时间戳,scc 表示节点所在的强连通分量编号
bool inStack[MAXN]; // 记录节点是否在栈内
stack<int> S; // DFS 中使用的栈
int cntDfn, cntScc; // 时间戳和强连通分量编号计数器
void Tarjan(int u) {
dfn[u] = low[u] = ++cntDfn;
S.push(u); inStack[u] = true;
for (int v : G[u]) {
if (!dfn[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (inStack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) { // 当前节点所在的强连通分量
++cntScc;
int v;
do {
v = S.top(); S.pop(); inStack[v] = false;
scc[v] = cntScc;
} while (v != u);
}
}
int main() {
int n, m;
cin >> n >> m;
// 构造邻接表
for (int i = 1; i <= m; ++i) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
}
// Tarjan 求解强连通分量
for (int i = 1; i <= n; ++i) {
if (!dfn[i]) {
Tarjan(i);
}
}
// 统计每个强连通分量中的节点个数
int cnt[MAXN]; memset(cnt, 0, sizeof(cnt));
for (int i = 1; i <= n; ++i) {
cnt[scc[i]]++;
}
// 输出答案
cout << cntScc << endl;
for (int i = 1; i <= cntScc; ++i) {
cout << cnt[i] << endl;
}
return 0;
}
本题考查的是图的基本算法——强连通分量。掌握 Tarjan 算法可以为图论的其他算法打下基础,对于求解网络流中需要用到的最小割和最大流也有很大帮助。