tarjan学习笔记
Tarjan算法学习笔记
前言
注:本笔记经过ai修改
先膜拜tarjan爷爷 orz

Tarjan算法是图论中的经典算法,由Robert Tarjan提出,具有多种核心应用,主要包括:求强连通分量(SCC)、缩点操作、求割点、求割边(桥)、求边双连通分量、求点双连通分量,核心思想基于深度优先搜索(DFS),通过维护时间戳和回溯信息高效求解各类图论问题。
一、Tarjan缩点/强连通分量(SCC)
1.1 DFS生成树
要理解Tarjan算法,首先需掌握DFS生成树的概念:
深度优先搜索(DFS)是一种用于遍历或搜索树或图的算法。在执行深度优先搜索时,会优先沿着一条路径深入到不能再深入为止,然后回溯到最近的分叉点继续搜索。这种方式下,遍历过程中经历的顶点和边的组合就构成了深度优先生成树(dfs生成树)。
有向图的DFS生成树中,主要包含四种边:
-
树边(普通边):DFS中,通过此边向下递归找到未访问过节点的边,如图中黑色边。
-
横插边(横向连接边):DFS中,通过此边访问到已访问过的节点,且该节点不是当前节点的祖先节点,如图中蓝色边。
-
返祖边(跳回祖宗节点的边):DFS中,通过此边访问到已访问过的节点,且该节点是当前节点的祖先节点,如图中红色边。
-
前向边(超前边):DFS时,遇到指向子树中已访问节点的边,如图中绿色边。
1.2 缩点核心原理
Tarjan求强连通分量(SCC)的本质是一种优化的DFS,核心是维护三个关键数据结构/数组:
-
栈
stk_t:存储搜索树中尚未处理完毕的节点; -
dfn[u]:记录节点u在DFS过程中被访问的次序(时间戳); -
low[u]:记录节点u及其子树中能够回溯到的、最早进入栈的节点的时间戳。
遍历节点u的相邻节点j时,分三种情况处理:
-
j未被访问过:递归遍历j节点,回溯时用
low[j]更新low[u](子树的回溯能力继承给父节点); -
j已被访问过,且在栈中:用
dfn[j]更新low[u](j是u的祖先,可通过返祖边回溯); -
j已被访问过,且不在栈中:无需操作(j属于已处理完毕的强连通分量,与当前分量无关)。
关键判定:对于一个强连通分量,有且仅有一个节点u满足 dfn[u] == low[u],该节点是这个强连通分量中第一个被访问的节点(根节点)。当回溯到该节点时,说明找到了一个完整的强连通分量,将栈中该节点及其上方的所有节点弹出,缩为一个新的分量。
缩点完成后,通常会对缩点后的DAG图跑拓扑排序,用于解决后续的路径、最值等问题。
1.3 参考代码
int n, m;
vector<int> e[N];
int dfn[N], low[N], tot;
int stk[N], tt = 0;
bool st[N];
int cnt, g[N], id[N]; // cnt:强连通分量个数;g[cnt]:第cnt个分量的节点数;id[u]:节点u所属的分量编号
inline void tarjan(int u){
dfn[u] = low[u] = ++ tot;
stk[ ++ tt] = u;
st[u] = true;
for(register int j : e[u]){
if(!dfn[j]){
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(st[j]){
low[u] = min(low[u], dfn[j]);
}
}
if(dfn[u] == low[u]){ // 找到强连通分量的根节点
cnt ++;
int y;
do{
y = stk[tt --];
id[y] = cnt;
g[cnt] ++; // 统计该分量的节点数,可根据需求修改
st[y] = false;
}while(y != u);
}
}
二、Tarjan求割点
2.1 割点定义
割点:在一个无向图中,如果去掉某个顶点及其所有连接的边后,图的连通分量数量增加(即不再连通),那么这个顶点就是割点(又称割顶)。
2.2 普通判定方法
最朴素的思路:遍历每个节点,删除该节点及其所有边,判断图的连通性。若连通性发生变化,则该节点是割点。

如上图,删除2号节点后,图分为两个连通分量,因此2号节点是割点。
2.3 Tarjan求割点原理
Tarjan求割点同样基于DFS,沿用 dfn(时间戳)和 low(回溯最早时间戳)两个数组,核心判定逻辑如下:
在DFS访问到节点u时,图被u分为“已访问部分”和“未访问部分”。若u是割点,则存在未访问的节点,在不经过u的情况下,无法到达已访问部分。
具体判定条件:
-
对于非根节点u:若存在子节点j,满足
low[j] >= dfn[u],说明j及其子树无法回溯到u的祖先节点,删除u后,j及其子树会与原图分离,因此u是割点; -
对于根节点u:若其儿子数量
son > 1,说明删除根节点后,多个儿子子树会相互分离,因此u是割点。
2.4 参考代码(模板题)
int n, m;
vector<int> e[N];
int dfn[N], low[N], tot;
int stk[N], tt;
int cnt = 0; // 割点的个数
bool cut[N]; // cut[u]:标记u是否是割点
inline void tarjan(int u, int fa){ // fa:u的父节点,根节点fa=-1
dfn[u] = low[u] = ++ tot;
stk[ ++ tt] = u;
int son = 0; // 记录u的儿子数量(仅统计直接子节点)
for(register int j : e[u]){
if(!dfn[j]){
son ++;
tarjan(j, u);
low[u] = min(low[u], low[j]);
// 非根节点,满足割点判定条件
if(low[j] >= dfn[u] && fa != -1){
if(!cut[u]) cnt ++;
cut[u] = true;
}
}
else{
// 回溯更新low[u](排除父节点,避免重复计算)
low[u] = min(low[u], dfn[j]);
}
}
// 根节点,儿子数>1则为割点
if(son > 1 && fa == -1){
if(!cut[u]) cnt ++;
cut[u] = true;
}
}
三、Tarjan求割边(桥)
3.1 割边定义
割边:在一个无向图中,如果去掉某条边后,图的连通分量数量增加(即不再连通),那么这条边就是割边(又称桥)。
3.2 割边示意图

如上图,删除2-4这条边后,图分为两个连通分量,因此2-4是割边。
3.3 Tarjan求割边原理
与割点判定类似,Tarjan求割边同样基于 dfn 和 low 数组,核心区别在于判定条件:
对于边 (u, j)(u是j的父节点),若满足 low[j] > dfn[u],说明j及其子树无法回溯到u或u的祖先节点,删除边 (u, j) 后,j及其子树会与原图分离,因此这条边是割边。
注:本代码为不带重边的割边判定,若存在重边,需额外处理(避免将重边误判为割边)。
3.4 参考代码
int n, m;
vector<int> e[N];
int dfn[N], low[N], tot;
int stk[N], tt;
int num = 0; // 割边的个数
inline void tarjan(int u, int fa){
dfn[u] = low[u] = ++ tot;
stk[ ++ tt] = u;
for(register int j : e[u]){
if(!dfn[j]){
tarjan(j, u);
low[u] = min(low[u], low[j]);
// 割边判定条件:子节点无法回溯到当前节点及以上
if(low[j] > dfn[u]){
num ++;
}
}
else if(j != fa){ // 排除父节点,避免重复计算
low[u] = min(low[u], dfn[j]);
}
}
}
四、Tarjan求边双连通分量
4.1 边双连通分量定义
边双连通分量(Biconnected Component, BCC):无向图中,不含割边的极大连通子图。即子图中任意两点之间至少存在两条不重合的边(去掉任意一条边,子图仍连通)。
4.2 参考代码(模板题)
模板题:P8436 边双连通分量模板
int n, m;
vector<PII> e[N]; // e[u] 存储 (邻接节点j, 边的编号)
int dfn[N], low[N], tot;
int stk[N], tt = 0;
bool st[N]; // 标记节点是否在栈中
int cnt, g[N], id[N]; // cnt:边双连通分量个数;id[u]:节点u所属分量编号
vector<vector<int>> ans; // 存储每个边双连通分量的节点
inline void tarjan(int u, int lt){ // lt:当前边的编号,用于排除反向边(避免重边误判)
dfn[u] = low[u] = ++ tot;
stk[ ++ tt] = u;
st[u] = true;
for(auto it : e[u]){
int j = it.first;
if(it.second == (lt ^ 1)) continue; // 排除反向边(假设边编号从0开始,异或1得到反向边)
if(!dfn[j]){
tarjan(j, it.second);
low[u] = min(low[u], low[j]);
}
else if(st[j]){
low[u] = min(low[u], dfn[j]);
}
}
if(dfn[u] == low[u]){ // 找到边双连通分量的根节点
cnt ++;
int y;
vector<int> v;
do{
y = stk[tt --];
id[y] = cnt;
v.push_back(y);
st[y] = false;
}while(y != u);
ans.push_back(v); // 存储当前分量的所有节点
}
}
五、Tarjan求点双连通分量
5.1 点双连通分量定义
点双连通分量(Vertex Biconnected Component, VBCC):无向图中,不含割点的极大连通子图。即子图中任意两点之间至少存在两条不经过同一个节点的路径(去掉任意一个节点,子图仍连通)。
5.2 参考代码(模板题)
模板题:P8435 点双连通分量模板
int n, m;
vector<int> e[N];
int dfn[N], low[N], stk[N], tt = 0;
int tot = 0; // 时间戳计数器
bool st[N]; // 标记节点是否在栈中
int cnt = 0; // 点双连通分量个数
int rt; // 根节点
bool ct[N]; // 标记节点是否是割点
vector<int> ans[N]; // ans[cnt] 存储第cnt个点双连通分量的节点
inline void tarjan(int u, int fa){
dfn[u] = low[u] = ++ tot;
stk[ ++ tt] = u;
// 特殊情况:孤立节点(单独作为一个点双连通分量)
if(fa == -1 && e[u].size() == 0){
ans[ ++ cnt].push_back(u);
return ;
}
st[u] = true;
int son = 0;
for(register int j : e[u]){
if(!dfn[j]){
son ++;
tarjan(j, u);
low[u] = min(low[u], low[j]);
// 点双连通分量判定,同时标记割点
if(low[j] >= dfn[u]){
son ++;
if(u != rt || son > 1){
ct[u] = true; // 标记u为割点
}
cnt ++;
int y;
do{
y = stk[tt --];
ans[cnt].push_back(y);
}while(y != j);
ans[cnt].push_back(u); // 割点会被包含在多个点双连通分量中
}
}
else if(j != fa){ // 排除父节点,避免重复计算
low[u] = min(low[u], dfn[j]);
}
}
}
六、相关练习题(按知识点分类)
1. 最朴素的Tarjan(强连通分量基础)
- 1. 信息传递
2. Tarjan求割点
3. Tarjan求割边
4. Tarjan求边双连通分量
- 1. 边双连通分量(模板)
5. Tarjan求点双连通分量
6. 缩点应用(点双/边双/强连通缩点)
- 1. 分离的路径

浙公网安备 33010602011771号