Tarjan 算法找强连通分量
什么是强连通分量?
连通: 对无向图而言,任意两点之间存在路径
强连通: 对有向图而言,任意两点之间存在路径
弱连通: 对有向图而言,将其视为无向图后任意两点之间存在路径
两种 dfs
第一种: 先访问当前节点,递再归出点
第二种: 先递归出点,再访问当前节点
⚠️注意:
每次到达时要做已到达的标记(而不是访问时标记),否则 dfs 会死循环. (这里的"访问"指输出之类的操作)
Tarjan 流程
时间戳: 每个节点带有时间戳标记(i,j),其中 i 表示 dfs 中到达该点的时间点,j 表示该点通过有向边可以回溯到的最早时间点.
- 到达一点时设置时间戳(到达时 i 和 j 相同),并将该点放入栈中
- 访问点时遍历其出点,更新 j
- 如果 j 不能更新(i==j),从栈顶开始出栈,构成一个强连通分量
🔮个人理解:
i 相当于节点的标号.
如果 j 不能更新则从栈顶开始出栈,表示这几个点可以相互到达,但不能到达更早的点.
⏱️时间复杂度:
由于完成 Tarjan 算法相当于执行依次完整的 dfs,时间复杂度为(使用邻接矩阵)或(使用邻接表).
Code实现(c++)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005; // 最大点数
vector<int> graph[MAXN]; // 邻接表
int dfn[MAXN], low[MAXN]; // 访问时间点和回溯到的最早时间点
bool inStack[MAXN]; // 标记是否在栈中
int timestamp; // 标记时间点
stack<int> stk;
int sccCount; // 强连通分量个数
vector<vector<int>> sccs; // 存储强连通分量
void tarjan(int x) {
dfn[x] = low[x] = ++timestamp; // 初始化时间戳
stk.push(x); // 入栈
inStack[x] = true;
for (int v : graph[x]) { // 遍历x的出点
if (!dfn[v]) { // 如果没到达过v
tarjan(v); // 从v开始dfs
low[x] = min(low[x], low[v]); // 要返回时更新x的low
} else if (inStack[v]) { // v已到达过但可能在同一分量中,只更新x的low
low[x] = min(low[x], low[v]);
}
}
if (dfn[x] == low[x]) { // x的low无法更新时,确定强连通分量
vector<int> scc; // scc是当前的强连通分量
int v = stk.top();
while (v != x) { // 从栈顶开始出栈,直到x
stk.pop();
inStack[v] = false;
scc.push_back(v);
v = stk.top();
}
stk.pop(); // 将x弹出
inStack[x] = false;
scc.push_back(x);
sccs.push_back(scc);
sccCount++;
}
}
int main() {
int V, E; // 点数,边数
cin >> V >> E;
for (int i = 1; i <= E; i++) { // 读入边
int u, v;
cin >> u >> v;
graph[u].push_back(v);
}
for (int i = 1; i <= V; i++) {
if (!dfn[i]) // dfn[i]==0表示i没到达过
tarjan(i);
}
cout << "Total Sccs: " << sccCount << '\n';
for (auto& scc : sccs) {
for (int x : scc) {
cout << x << ' ';
}
cout << '\n';
}
return 0;
}
PS:讲解思路来源于b站