Tarjan 算法的两种用法
Tarjan 算法:
利用 DFS 过程中记录的时间戳和追溯值来发现图的特定结构
- 时间戳(dfn [u])
- 表示节点
u
在 DFS 中被首次访问的顺序(访问顺序编号)。
- 表示节点
- 追溯值(low [u])
- 表示节点
u
或其子孙能通过非父子边追溯到的最早时间戳的节点,计算方式:low[u] = dfn[u]
(初始值)- 遍历所有邻接节点
v
:- 若
v
是父节点,跳过; - 若
v
未访问,递归访问后更新low[u] = min(low[u], low[v])
; - 若
v
已访问且在当前栈中,更新low[u] = min(low[u], dfn[v])
。
- 若
- 表示节点
- 栈结构
- 用于记录当前 DFS 路径上的节点,判断强连通分量或割点等结构。
Tarjan 求强连通分量
-
强连通节点对
对于有向图中的两个节点 u 和 v,若存在从 u 到 v 的有向路径,同时也存在从 v 到 u 的有向路径,则称 u 和 v 是强连通的。- 例:在有向环 A→B→C→A 中,任意两点间都有双向路径,因此 、、 两两强连通。
-
强连通图
若有向图中任意两个节点都强连通,则称该图为强连通图。- 例:单个有向环是典型的强连通图;若图中存在多个环且环间无连接,则不是强连通图。
-
强连通分量(Strongly Connected Component, SCC)
有向图中的极大强连通子图,即:- 该子图内任意两点强连通;
- 不存在更大的子图包含它且满足强连通条件。
算法原理
Tarjan 算法基于对图进行 DFS,视每个连通分量为搜索树中的一棵子树,在搜索过程中维护一个栈,每次把搜索树中尚未处理的节点加入栈中。
#include<bits/stdc++.h>
using namespace std;
int dfn[N], low[N], dfncnt, s[N], in_stack[N], tp; // dfn为时间戳,low为子树最小时间戳,dfncnt为时间戳计数器,s为栈,in_stack标记是否在栈中,tp为栈顶指针
int scc[N], sc; // scc记录每个节点所属强连通分量编号,sc为强连通分量计数器
int sz[N]; // 记录每个强连通分量的大小void tarjan(int u) { low[u] = dfn[u] = ++dfncnt; // 初始化时间戳和子树最小时间戳s[++tp] = u; // 将节点入栈in_stack[u] = 1; // 标记节点在栈中for (int i = h[u]; i; i = e[i].nex) { // 遍历u的所有邻接边const int &v = e[i].t; // 邻接节点vif (!dfn[v]) { // 如果v未被访问tarjan(v); // 递归访问vlow[u] = min(low[u], low[v]); // 更新u的子树最小时间戳} else if (in_stack[v]) { // 如果v在栈中(属于当前强连通分量)low[u] = min(low[u], dfn[v]); // 更新u的子树最小时间戳}}// 当u是强连通分量的根节点时if (dfn[u] == low[u]) { ++sc; // 强连通分量计数器加1do { scc[s[tp]] = sc; // 记录节点所属强连通分量sz[sc]++; // 强连通分量大小加1in_stack[s[tp]] = 0; // 标记节点出栈} while (s[tp--] != u); // 弹出栈中节点直到遇到u}
}
Tarjan 算法来求解无向图中的所有桥(割边)。
桥是指在一个无向图中,如果删除某条边后,图的连通分量数目增加,则称该边为桥。
- 核心思想:利用 DFS 遍历,通过记录节点的时间戳(dfn)和 “追溯值”(low)判断割点。
- 关键条件(设 u 为当前节点,v 为 u 的子节点):
- 情况 1:若 u 是 DFS 树的根节点,且 u 有至少两个子节点(删除 u 后子树分裂),则 u 是割点;
- 情况 2:若 u 不是根节点,且存在子节点 v 满足 low[v]≥dfn[u],则 u 是割点(说明 v 的子树无法通过非父边回到 u 的祖先,删除 u 后 v 的子树与其他部分断开)。
- 算法步骤:
- 初始化 dfn 和 low 数组,时间戳从 1 开始;
- 对每个未访问节点启动 DFS,遍历过程中更新 low[u] 为 dfn[u] 与所有邻接节点 low 的最小值;
- 对每个子节点 v,若 v 是父节点则跳过,否则递归处理 v,并更新 low[u]=min(low[u],low[v]);
- 回溯时检查上述割点条件。
#include <bits/stdc++.h>
using namespace std;
int maps[151][151]; // 邻接矩阵存储图结构
struct Edge { // 边的结构体,用于存储桥int x,y;
} E[5001];
int dfn[151],low[151],n,m,id,cnt,f[151];
// dfn[]: 时间戳数组,表示节点被访问的顺序
// low[]: 追溯值数组,表示从当前节点出发,能够追溯到的最早时间戳
// n: 节点数,m: 边数,id: 时间戳计数器,cnt: 桥的数量
// f[]: 存储每个节点的父节点// 边的比较函数,用于排序
bool cmp(struct Edge a,struct Edge b) {if(a.x==b.x)return a.y<b.y;return a.x<b.x;
}// 添加桥到结果数组
void addEdge(int x,int y){E[++cnt].x=x;E[cnt].y=y;
}// Tarjan算法核心函数,寻找桥
void tarjan(int x){int c=0,y;dfn[x]=low[x]=++id; // 初始化时间戳和追溯值for(register int i=1; i<=n; i++) { // 遍历所有节点if(!maps[x][i])continue; // 如果节点i与x之间没有边,跳过y=i;// 如果节点i已经被访问过且不是当前节点的父节点if(dfn[y]&&y!=f[x])low[x]=min(low[x],dfn[y]);// 如果节点i未被访问过if(!dfn[y]) {f[y]=x; // 设置父节点tarjan(y); // 递归处理子节点low[x]=min(low[x],low[y]); // 更新追溯值// 如果子节点的追溯值大于当前节点的时间戳,则(x,y)是桥if(low[y]>dfn[x])addEdge(x,y);}}
}int main() {int x,y;cin>>n>>m; // 输入节点数和边数// 读入每条边,构建邻接矩阵for(register int i=1; i<=m; i++) {cin>>x>>y;maps[x][y]=maps[y][x]=1; // 无向图,双向标记}// 对每个未访问的节点调用Tarjan算法for(register int i=1; i<=n; i++) {if(!dfn[i])tarjan(i);}// 对找到的桥进行排序sort(E+1,E+cnt+1,cmp);// 输出每条桥,按节点编号从小到大的顺序for(register int i=1; i<=cnt; i++) {cout<<min(E[i].x,E[i].y)<<' '<<max(E[i].x,E[i].y)<<endl;}return 0;
}