一、算法概述
- 定义:Tarjan算法是一种用于在有向图中求解强连通分量(Strongly Connected Component, SCC)的高效算法。强连通分量指有向图中任意两顶点互相可达的最大子图。
- 核心思想:基于深度优先搜索(DFS),利用时间戳和栈结构,通过追踪顶点的访问顺序和回溯能力,识别强连通分量。
- 应用场景:
- 有向图缩点(将每个强连通分量缩为一个点,构建DAG)
- 电路设计中的环路检测
- 任务调度中的依赖关系分析
二、算法思路
- 关键变量:
dfn[u]
:顶点u
的首次访问时间戳low[u]
:顶点u
能回溯到的最早时间戳(通过有向边)inStack[u]
:标记顶点u
是否在栈中stack
:存储当前DFS路径上的顶点
- 核心逻辑:
- 对每个未访问顶点启动DFS,记录时间戳
dfn[u]
和low[u]
- 若顶点
v
已访问且在栈中,更新low[u]
为min(low[u], dfn[v])
- 当
dfn[u] == low[u]
时,栈中从u
开始的连续顶点构成一个强连通分量
- 对每个未访问顶点启动DFS,记录时间戳
- 强连通分量判定:当一个顶点的时间戳等于其能回溯到的最早时间戳时,该顶点是所在强连通分量的根节点,此时从栈中弹出直至该顶点,构成一个SCC。
三、伪代码实现
1. 类定义与数据结构
类 TarjanSCC:
私有变量:
n: 整数 # 图的顶点数
adj: 二维数组 # 邻接表,adj[u]存储u的所有出边
dfn: 数组 # 顶点访问时间戳
low: 数组 # 顶点能回溯到的最早时间戳
inStack: 数组 # 标记顶点是否在栈中
st: 栈 # 存储当前路径顶点
timeStamp: 整数 # 时间戳计数器
sccId: 数组 # 顶点所属的强连通分量编号
sccCount: 整数 # 强连通分量数量
sccNodes: 二维数组 # 每个强连通分量包含的顶点列表
公有方法:
构造函数 TarjanSCC(_n):
n = _n
adj 调整大小为 n+1
dfn 调整大小为 n+1,初始化为 0
low 调整大小为 n+1,初始化为 0
inStack 调整大小为 n+1,初始化为 false
sccId 调整大小为 n+1,初始化为 0
timeStamp = 0
sccCount = 0
sccNodes 清空
方法 addEdge(u, v):
将 v 添加到 adj[u] 中
方法 solve():
对 i 从 1 到 n:
如果 dfn[i] 为 0:
tarjanDFS(i)
方法 getSCCCount():
返回 sccCount
方法 getSCCId(u):
返回 sccId[u]
方法 getSCCNodes(sccId):
返回 sccNodes[sccId]
私有方法:
方法 tarjanDFS(u):
dfn[u] = low[u] = ++timeStamp
将 u 压入栈 st
inStack[u] = true
对 i 从 0 到 adj[u].长度-1:
v = adj[u][i]
如果 dfn[v] 为 0:
tarjanDFS(v)
low[u] = min(low[u], low[v])
否则如果 inStack[v] 为 true:
low[u] = min(low[u], dfn[v])
如果 dfn[u] == low[u]:
新建空数组 scc
v = -1
循环:
v = st.top()
st.pop()
inStack[v] = false
sccId[v] = sccCount
将 v 添加到 scc 中
直到 v == u
将 scc 添加到 sccNodes 中
sccCount++
2. 主程序示例
输入 n, m # 顶点数和边数
创建 TarjanSCC 实例 ts(n)
对 i 从 0 到 m-1:
输入 u, v
ts.addEdge(u, v)
ts.solve()
输出 "强连通分量数量: " + ts.getSCCCount()
对 i 从 0 到 ts.getSCCCount()-1:
输出 "SCC " + i + ": " + ts.getSCCNodes(i)
四、算法解释
1. 初始化阶段
- 构造函数中初始化邻接表、时间戳数组、栈和强连通分量记录结构,为后续计算做准备。
2. DFS遍历与时间戳更新
tarjanDFS(u)
中,首次访问u
时记录dfn[u]
和low[u]
为当前时间戳。- 遍历
u
的所有邻接顶点v
:- 若
v
未访问,递归访问v
,并通过low[v]
更新low[u]
(说明u
可通过v
回溯到更早的顶点)。 - 若
v
已访问且在栈中,说明存在环,通过dfn[v]
更新low[u]
(v
在当前路径上)。
- 若
3. 强连通分量识别
- 当
dfn[u] == low[u]
时,u
是当前强连通分量的根节点。 - 从栈中弹出顶点直至
u
,这些顶点构成一个强连通分量,记录其编号和包含的顶点。
4. 示例演示
- 图结构:有向图顶点1→2,2→3,3→1,2→4,4→5,5→4
- DFS过程:
- 访问1,
dfn[1]=low[1]=1
,压栈。 - 访问2,
dfn[2]=low[2]=2
,压栈,访问3。 - 访问3,
dfn[3]=low[3]=3
,压栈,发现3→1,1在栈中,low[3] = min(3, dfn[1])=1
。 - 返回2,
low[2] = min(2, low[3])=1
,访问4,dfn[4]=low[4]=4
,压栈,访问5。 - 访问5,
dfn[5]=low[5]=5
,发现5→4,4在栈中,low[5] = min(5, dfn[4])=4
。 - 返回4,
low[4] = min(4, low[5])=4
,此时dfn[4]==low[4]
,弹出4、5,形成SCC{4,5}。 - 返回2,
low[2]=1
,返回1,dfn[1]==low[1]
,弹出1、2、3,形成SCC{1,2,3}。
- 访问1,
五、复杂度分析
- 时间复杂度:
- 每个顶点和边仅被访问一次,时间复杂度为O(n + m),其中n为顶点数,m为边数。
- 空间复杂度:
- 邻接表存储边:O(m)
- 数组和栈存储状态:O(n)
- 总空间复杂度为O(n + m)。
- 算法优势:
- 线性时间复杂度,适用于大规模有向图的强连通分量求解。
- 仅使用常数额外空间,空间效率高。
- 应用扩展:
- 缩点后可在DAG上进行拓扑排序、最短路径等操作。
- 结合2-SAT问题求解布尔表达式的可满足性。
本文为作者(英雄哪里出来)在抖音的独家课程《英雄C++入门到精通》、《英雄C语言入门到精通》、《英雄Python入门到精通》三个课程的配套文字讲解,如需了解算法视频课程,请移步 作者本人 的抖音直播间。