1. 基本概念
1.1 强连通分量(Strongly Connected Component, SCC)
在有向图中,如果两个顶点 u 和 v 之间存在从 u 到 v 的路径,也存在从 v 到 u 的路径,则称 u 和 v 是强连通的。一个有向图的极大强连通子图称为强连通分量。
1.2 Tarjan 算法的核心思想
利用 DFS 遍历图,通过维护每个节点的两个关键值来识别强连通分量:
dfn[u]:节点 u 的 DFS 访问序号(时间戳)low[u]:以 u 为根的搜索子树中,能通过后向边追溯到的最早节点的 dfn 值
2. 算法原理
2.1 关键数据结构
vector<vector<int>> graph; // 邻接表表示的图
vector<int> dfn; // 访问时间戳
vector<int> low; // 能追溯到的最早时间戳
vector<bool> inStack; // 节点是否在栈中
stack<int> st; // 存储当前 DFS 路径上的节点
int timestamp; // 时间戳计数器
2.2 核心逻辑
- 从任意未访问节点开始 DFS
- 为当前节点分配时间戳,入栈
- 遍历所有邻接节点:
- 未访问:递归处理,更新 low 值
- 已访问且在栈中:更新 low 值
- 已访问但不在栈中:跳过
- 如果
dfn[u] == low[u],说明找到一个强连通分量
3. C++ 实现
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
class TarjanSCC {
private:
vector<vector<int>> graph;
vector<int> dfn, low;
vector<bool> inStack;
stack<int> st;
int timestamp;
vector<vector<int>> sccs; // 存储所有强连通分量
void tarjan(int u) {
// 为节点 u 分配时间戳
dfn[u] = low[u] = ++timestamp;
st.push(u);
inStack[u] = true;
// 遍历所有邻接节点
for (int v : graph[u]) {
if (!dfn[v]) {
// 节点 v 未被访问过,递归处理
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (inStack[v]) {
// 节点 v 已被访问且在栈中,更新 low 值
low[u] = min(low[u], dfn[v]);
}
// 如果 v 已被访问但不在栈中,说明 v 属于其他 SCC,跳过
}
// 如果 dfn[u] == low[u],说明找到了一个 SCC 的根节点
if (dfn[u] == low[u]) {
vector<int> scc;
int w;
// 从栈中弹出属于当前 SCC 的所有节点
do {
w = st.top();
st.pop();
inStack[w] = false;
scc.push_back(w);
} while (w != u);
sccs.push_back(scc);
}
}
public:
TarjanSCC(int n, const vector<vector<int>>& edges) {
// 构建邻接表
graph.resize(n);
for (const auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
}
// 初始化数据结构
dfn.assign(n, 0);
low.assign(n, 0);
inStack.assign(n, false);
timestamp = 0;
}
vector<vector<int>> findSCCs() {
sccs.clear();
// 对每个未访问的节点进行 Tarjan 算法
for (int i = 0; i < graph.size(); i++) {
if (!dfn[i]) {
tarjan(i);
}
}
return sccs;
}
};
// 测试函数
void testTarjan() {
// 构建测试图
// 图结构:
// 0 -> 1 -> 2 -> 3
// ^ | |
// | v v
// +---------4 5
vector<vector<int>> edges = {
{0, 1}, {1, 2}, {2, 3}, {3, 5},
{2, 4}, {4, 0}, {4, 5}
};
int n = 6; // 节点数 0-5
TarjanSCC solver(n, edges);
vector<vector<int>> sccs = solver.findSCCs();
cout << "找到 " << sccs.size() << " 个强连通分量:" << endl;
for (int i = 0; i < sccs.size(); i++) {
cout << "SCC " << i + 1 << ": ";
for (int node : sccs[i]) {
cout << node << " ";
}
cout << endl;
}
}
int main() {
testTarjan();
return 0;
}
4. 算法复杂度分析
-
时间复杂度:O(V + E)
- 每个节点和边只被访问一次
- DFS 遍历的时间复杂度
-
空间复杂度:O(V)
- 存储 dfn、low、inStack 数组:O(V)
- 递归栈深度:O(V)
- 结果存储:最坏情况下 O(V²)
5. 算法正确性证明
Tarjan 算法的正确性基于以下关键性质:
- 追溯性质:
low[u]正确反映了以 u 为根的子树中能追溯到的最早节点 - SCC 根性质:当
dfn[u] == low[u]时,u 是其所在 SCC 的"根"节点 - 栈性质:栈中从 u 到栈顶的节点恰好构成以 u 为根的 SCC
6. 应用场景
- 图的简化:将有向图缩点为 DAG
- 2-SAT 问题:判断布尔表达式的可满足性
- 网络分析:识别网络中的紧密连接子群
- 编译器优化:循环检测和优化
- 社交网络分析:发现紧密联系的用户群体
7. 算法变种
Tarjan 算法还可用于:
- 求解无向图的割点和桥
- 求解双连通分量
- 求解 2-SAT 问题
Tarjan算法:强连通分量识别利器
6722

被折叠的 条评论
为什么被折叠?



