Tarjan 求强连通分量和缩点
\(\texttt{Upd 23.8.29}\) 修正错别字、证明,添加代码和注释
欢迎批评指正!
注意:本文只针对有向图。
前置芝士
- 什么是强连通分量(SCC)?
强连通分量,一般指 有向图的极大强连通子图,在这些子图中,所有点双向可达。 - dfs 序:即 dfs 过程中访问点的顺序。
- dfs 生成树:由 dfs 过程中访问的边组成的边集 和 原图的点集 组成的树。
- 树边,非树边:属于 dfs 过程中访问的边 为树边,否则为非树边。
- 返祖边(反向边):从孩子连接到祖先的边。
- 前向边:从祖先连接到孩子的边。
- 横插边:除返祖边和前向边之外的非树边。(连接没有祖孙关系的两点的边。)
- 某个强连通分量的根:这个强连通分量重 dfs 序最小的节点。
Tarjan 算法
设两个数组 \(\mathit{dfn}\) 和 \(\mathit{low}\),分别表示 dfs 序和至多通过 \(1\) 条非树边所能到达的点的 \(\mathit{dfn}\) 的最小值。
看下面这张图:
每个节点的编号就是它的 \(\mathit{dfn}\),旁边标注的数字是 \(\mathit{low}\),可以自行理解一下。
另外,我们还需要一个栈 \(\mathit{stk}\) 存节点。
算法流程
Tarjan 是在 dfs 中实现的,每次访问到当前节点 \(u\),就将它压入 \(\mathit{stk}\) 中。随后访问所有相邻的点 \(v\)。
-
如果没访问过,说明 \((u,v)\) 是一条树边,继续 dfs。
dfs \(v\) 的子树结束后,更新 \(\mathit{low}_u\gets\min(\mathit{low}_u,\mathit{low}_v)\)。因为根据 \(\mathit{low}\) 的定义,走多少条树边都没关系,而 \((u,v)\) 是一条树边,我们可以从 \(u\) 走到 \(v\) 后再继续往上爬。也就是说,\(v\) 能到的 \(\mathit{low}_v\),\(u\) 也能到。于是更新。
-
如果 \(v\) 已被访问过且已属于另一个 SCC,说明 \((u,v)\) 是一条横插边,两个 SCC 无关,直接跳过,不予处理。
-
如果 \(v\) 被访问过且在 \(\mathit{stk}\) 内,说明 \((u,v)\) 是一条返祖边,更新 \(\mathit{low}_u\gets\min(\mathit{low}_u,\mathit{dfn}_v)\)。
因为 \(\mathit{low}\) 的定义为仅通过 \(1\) 条非树边,故可以直接走返祖边 \((u,v)\),如果 \(\mathit{dfn}_v\) 比 \(\mathit{low}_u\) 小,则更新。
关于为什么不是 \(\mathit{low}_u\gets\min(\mathit{low}_u,\mathit{low}_v)\),因为 \(\mathit{low}_v\) 可能已经经过了一条非树边,如果再走返祖边 \((u,v)\),就可能走了两条非树边,与 \(\mathit{low}\) 的定义相悖。(其实这样些也可以得到正确答案,但是求割点时就会错。)
-
然后判断 \(u\) 是否是根:若 \(\mathit{dfn}_u=\mathit{low}_u\),则是当前 SCC 的根,然后退栈到 \(u\),保存答案。
因为 \(\mathit{dfn}_u=\mathit{low}_u\),所以 \(u\) 的子树内的所有点都不能通过返祖边到达 \(\mathit{dfn}\) 比 \(u\) 小的点(否则可以通过树边到达这些子节点,然后走返祖边。使 \(\mathit{low}_u<\mathit{dfn}_u\)。)
关于为什么跳出子树至多经过 \(1\) 条非树边:如果有 \(2\) 条甚至更多,必然有走的最后一条边跳出了子树。那么跳出子树前的点必然在子树内,可以走树边到达,故至多(其实是要且只要)走 \(1\) 条非树边。
代码
int n,m; vector<int> e[11451],stk;// e:边表 int bel[11451];// bel[i] 表示 i 属于哪个 SCC(编号) vector<vector<int>> scc={{}};// 存每个强连通分量的点的编号 int cnt=1,dfn[11451],low[11451];// cnt:SCC数量(+1) int dfncnt=1;// 时间戳 void tarjan(int u) { dfn[u]=low[u]=dfncnt++; stk.push_back(u); for(int v:e[u])// 范围 for,遍历邻居 { if(!dfn[v])// 未访问 { tarjan(v); low[u]=min(low[u],low[v]);// 访问并更新 } else if(!bel[v])// in stk { low[u]=min(low[u],dfn[v]);// 单纯更新 } } if(low[u]==dfn[u])// 是当前 SCC 的根 { int t; scc.push_back({}); while(stk.back()!=u)// 退栈到 u { t=stk.back(); stk.pop_back(); scc[cnt].push_back(t);// 保存 bel[t]=cnt; } scc[cnt].push_back(u); sort(scc[cnt].begin(),scc[cnt].end()); bel[u]=cnt++; stk.pop_back(); } }
注意图可能不连通,所以主函数里要加上:
for(int i=1;i<=n;i++) { if(!dfn[i]) { tarjan(i); } }
每次都调用一遍 tarjan
。
缩点
缩点很好理解,就是将每个强连通分量中的点的信息合并,缩成一个点,形成一个 DAG(有向无环图)。
代码
for(int i=1;i<=n;i++) { newval[bel[i]]+=val[i]; for(int j:e[i]) { if(bel[i]!=bel[j]) { newe[bel[i]].push_back(bel[j]); } } }
其中 e
为原图,newe
为新图,val
为原图点权(或者什么类似点权的值),newval
为新图点权。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验