视频加载失败

Tarjan 算法求强连通分量

813 字
4 分钟
Tarjan 算法求强连通分量

Tarjan 算法是一种用于 求强连通分量(SCC, Strongly Connected Components) 的高效算法,时间复杂度 O(n + m),其中 nn 是顶点数,mm 是边数。它基于 深度优先搜索(DFS),使用回溯的思想来判断一个点是否属于同一个 SCC。

1. Tarjan 算法核心思路#

  1. 时间戳(dfn):记录每个节点的 DFS 访问次序,每个点的 dfn[u] 值是其访问顺序(从 1 开始递增)。
  2. 最小可达值(low):记录 当前节点能通过 DFS 访问到的最早的节点。
  • 如果 low[u] == dfn[u],说明 u 是一个 SCC 的根,栈中 u 及其上方的所有点属于同一 SCC。
  1. 栈(stack):维护 SCC,确保 SCC 内的点按拓扑顺序排列。
  2. 入栈标记(in_stack):用于判断当前节点是否在栈中,避免重复计算。

2. Tarjan 代码框架#

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5; // 最大节点数
vector<int> graph[N]; // 邻接表存储图
int dfn[N], low[N], scc_id[N], ts = 0, scc_cnt = 0;
bool in_stack[N];
stack<int> stk;
void tarjan(int u) {
dfn[u] = low[u] = ++ts; // 设置访问次序
stk.push(u);
in_stack[u] = true;
for (int v : graph[u]) {
if (!dfn[v]) { // 递归访问
tarjan(v);
low[u] = min(low[u], low[v]); // 回溯更新 low 值
} else if (in_stack[v]) { // 发现回边
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) { // 发现 SCC 根
++scc_cnt;
while (true) {
int v = stk.top();
stk.pop();
in_stack[v] = false;
scc_id[v] = scc_cnt; // 给 SCC 赋编号
if (v == u) break;
}
}
}
void find_scc(int n) {
for (int i = 1; i <= n; i++) {
if (!dfn[i]) tarjan(i);
}
}

3. Tarjan SCC 代码详解#

(1) dfn[u] 和 low[u]#

  • dfn[u]:表示 访问次序,用递增的时间戳标记。
  • low[u]:表示 回溯能到的最早访问点。
  • 更新规则:

low[u] = min(low[u], low[v])(如果 v 是 u 的子节点)。 low[u] = min(low[u], dfn[v])(如果 v 已访问,且 v 在栈中)。

(2) SCC 识别#

  • 如果 dfn[u] == low[u],说明 u 是 SCC 根,栈中 u 及其上方的所有点属于同一 SCC。
  • SCC 出栈的顺序即拓扑序,可以用于 缩点(Condensation Graph)。

4. SCC 相关应用#

(1) 求缩点 DAG#

SCC 形成后,我们可以 建立一个新的 DAG(有向无环图),以 SCC 为点,SCC 之间有向边:

vector<int> scc_graph[N]; // SCC 缩点后的图
vector<int> scc_in_degree(N, 0); // SCC 入度
void build_scc_graph(int n) {
for (int u = 1; u <= n; u++) {
for (int v : graph[u]) {
if (scc_id[u] != scc_id[v]) { // 仅当 u, v 属于不同 SCC 时建边
scc_graph[scc_id[u]].push_back(scc_id[v]);
scc_in_degree[scc_id[v]]++;
}
}
}
}

(2) 统计有向图中的「强连通块」#

Tarjan 算法可以用于求解 一个图最多能划分成多少个强连通块:

cout << "图的强连通分量个数: " << scc_cnt << endl;

(3) 计算入度为 0 的 SCC 数量#

入度为 0 的 SCC 在缩点 DAG 中没有前驱,可用于拓扑排序:

int zero_in_degree_scc = 0;
for (int i = 1; i <= scc_cnt; i++) {
if (scc_in_degree[i] == 0) {
zero_in_degree_scc++;
}
}

(4) 计算强连通分量中的最小代价#

有时 SCC 内有一些数值(如贿赂代价、最小值等),可以用 SCC 合并后取最小值:

vector<int> scc_min_cost(scc_cnt + 1, INF);
for (int i = 1; i <= n; i++) {
scc_min_cost[scc_id[i]] = min(scc_min_cost[scc_id[i]], cost[i]);
}

5. 复杂度分析#

  • Tarjan 的时间复杂度:

每个点 被访问 1 次(O(n))。 每条边 被遍历 1 次(O(m))。 总时间复杂度 O(n + m),适用于 大规模数据(如 n,m≤105n, m \leq 10^5)。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Tarjan 算法求强连通分量
https://blog.81vm3.xyz/posts/tarjan/
作者
Blume
发布于
2025-01-12
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Blume
I build interesting things.
公告
欢迎来到我的博客!
分类
标签
最新动态
站点统计
文章
38
分类
5
标签
98
总字数
23,078
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录