V

算法可视化

ZHEN
分享到微博分享到 XGitHub 仓库个人主页
学习工具算法复杂度速查算法学习路径
数据结构数组链表栈队列树堆哈希表图字典树并查集LRU 缓存跳表线段树B+ 树布隆过滤器树状数组
经典排序算法冒泡排序鸡尾酒排序双调排序选择排序插入排序二分插入排序希尔排序归并排序自顶向下归并快速排序三路快排双轴快排堆排序计数排序基数排序桶排序
图算法Dijkstra 最短路Kruskal 最小生成树Prim 最小生成树Bellman-Ford 最短路拓扑排序Floyd 多源最短路强连通分量2-SAT最大流二分图匹配LCA 倍增欧拉路径
动态规划编辑距离0-1 背包完全背包最长公共子序列最长递增子序列硬币找零方案数石子合并旅行商 TSP树形 DP数位 DP换根 DP
回溯与搜索N 皇后子集生成全排列组合总和迷宫寻路岛屿数量单词搜索数独A* 寻路
字符串KMP 字符串匹配Rabin-KarpBoyer-MooreManacher后缀数组LCP / height 数组AC 自动机Z 函数
数学与数论埃氏筛线性筛欧几里得算法快速幂扩展欧几里得中国剩余定理欧拉函数米勒-拉宾FFTPollard's Rho
计算几何凸包旋转卡壳最近点对线段相交扫描线求交
查找二分查找二分边界旋转数组搜索二分答案三分查找

强连通分量(Tarjan)

图算法 · 有向图连通性 · O(V+E)

能互相到达的极大集合

在有向图里,如果一组节点两两都能互相到达(你能到我、我也能到你),且再加任何一个节点都做不到,这组节点就构成一个强连通分量(SCC)。把每个 SCC 缩成一个点,有向图就变成一张无环图(DAG)——这是很多高级图算法的第一步。朴素做法对每个点各跑一次 DFS 判断可达性太慢;Tarjan 用一趟 DFS 就求出所有 SCC。

dfn、low 和一个栈

DFS 给每个节点记两个数:dfn[u]——它被发现的次序(时间戳);low[u]——从 u 的子树出发、至多经过一条回边能回溯到的最小 dfn。DFS 时把访问到的节点压入一个栈。low 在两处更新:子节点递归返回时 low[u]=min(low[u], low[子]);遇到一条指向还在栈里的节点 v 的回边时 low[u]=min(low[u], dfn[v])。

关键判据:当 low[u] == dfn[u] 时,说明 u 回不到任何更早的祖先,它就是一个 SCC 的根——此时把栈里 u 之上(含 u)的节点全部弹出,它们正好构成一个强连通分量。因为每个节点、每条边只处理一次,总复杂度 O(V+E)。

下面是一张 6 节点有向图。点「下一步」逐步看:节点右上角是 dfn/low、虚线环是当前在栈里的节点、琥珀环是当前节点、绿边是 DFS 树边、黄边是当前回边;每弹出一个 SCC,它的节点就着上同一种颜色。走到底,三个 SCC {0,1,2}、{3,4}、{5} 三色分明。右侧代码随每一步同步高亮。

00/012345

访问节点 0:dfn=low=0,入栈

1function tarjan(n: number, adj: number[][]): number[][] {
2 const dfn = Array(n).fill(-1), low = Array(n).fill(-1);
3 const onStk = Array(n).fill(false), stk: number[] = [];
4 const sccs: number[][] = []; let idx = 0;
5 const dfs = (u: number) => {
6 dfn[u] = low[u] = idx++; // 发现 u:dfn=low=时间戳
7 stk.push(u); onStk[u] = true; // 入栈
8 for (const v of adj[u]) {
9 if (dfn[v] === -1) { // 树边:v 未访问
10 dfs(v);
11 low[u] = Math.min(low[u], low[v]); // 子树回传
12 } else if (onStk[v]) { // 回边:v 在栈中
13 low[u] = Math.min(low[u], dfn[v]);
14 }
15 }
16 if (low[u] === dfn[u]) { // u 是 SCC 根
17 const comp: number[] = []; let w;
18 do { w = stk.pop()!; onStk[w] = false; comp.push(w); }
19 while (w !== u); // 弹栈到 u → 一个 SCC
20 sccs.push(comp);
21 }
22 };
23 for (let i = 0; i < n; i++) if (dfn[i] === -1) dfs(i);
24 return sccs;
25}
节点/边6 点 / 7 有向边
当前节点0(dfn=0, low=0)
栈[0]
已找 SCC0
1 / 17

强连通分量在哪里用

缩点:把每个 SCC 缩成一点得到 DAG,再在 DAG 上跑 拓扑排序 / DP。
2-SAT:布尔约束可满足性判定,靠 SCC 判断 x 与 ¬x 是否同组。
依赖 / 死锁检测:有向环 = 循环依赖,SCC 一次找出所有环。

和 拓扑排序正好互补:拓扑排序处理的是无环有向图(DAG),而 Tarjan 专门把有环的部分(每个 SCC)识别出来——两者合起来,任何有向图都能先缩点、再拓扑。