Tarjan 算法
前言
说来惭愧,这个模板仅是绿的算法至今我才学会。
我还记得去年 CSP2023 坐大巴路上拿着书背 Tarjan 的模板(CSP2024 也没学会)。虽然那年没有考连通分量类似的题目(2024 也没有)。
现在做题遇到了 Tarjan,那么,重学,开写!
另,要想学好此算法的第一件事——膜拜 Tarjan 爷爷。
再序(upd on 2024.11.4)
这个东西意外成为了我博客下最多人收藏的文章,我在 CSP2024 前重新复习时发现仍然漏洞百出,为了对大家负责我决定重构整篇。
顺带补上边双和点双。
定义
强连通分量
一个图的子图若任意两点之间均有路径可达,则成该子图为原图的强连通分量。
割点
如果一个连通分量内有一个点,把这个点及其邻边删掉后连通分量不再连通而变成了两个连通分量,那么这个点就是割点。下图中点 2 2 2 就是割点。
割边(桥)
与割点定义类似,删除某一边后连通分量不再连通。如下图 ( 2 , 5 ) (2,5) (2,5) 和 ( 5 , 6 ) (5,6) (5,6) 都是割边。
点双连通分量
定义:没有割点的双连通分量。“双连通分量”指任意两点都有两条不同路径可达。
边双连通分量
定义:没有割边的双连通分量。
如何求割点
采用 Tarjan 求割点。思想:在 dfs 时访问到 k k k 点时,图会被点 k k k 分为已访问和未访问两部分。如果 k k k 是割点,那么一定存在没有访问过的点在不经过 k k k 的情况下不能访问到已访问过的点。
这里要用到回溯值 low[u]
表示点 u u u 可以访问到时间戳(dfn[u]=++tim
)最小的节点。
根据这些信息来判断割点:
- 若当前点是根节点,如果子树数量大于 1 1 1 则说明该点为割点。
- 若当前点不是根节点,如果存在一个儿子的回溯值大于等于该点的时间戳(
low[v]>=dfn[u]
),则该点为割点(因为儿子无法绕过该点去访问时间戳更小的点)。
如何求割边
判断割边的条件是一条边 ( u , v ) (u,v) (u,v) 有 low[v]>dfn[u]
。如果相等意味着点 v v v 还可以回到点 u u u,所以不相等意味着连父亲也回不到。如果点 v v v 回不到祖先,也无法绕过 ( u , v ) (u,v) (u,v) 这条边回到父亲,那么 ( u , v ) (u,v) (u,v) 就是割边。
如何求点双
点双是没有割点的双连通分量,一个割点可能属于多个点双,非割点只属于一个点双。那么我们可以在 dfs 的过程中压点入栈,判断到当前点为割点时,就不断弹栈,弹出来的节点都属于一个点双。
注意特判一个点就是一个子图的情况也是点双。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e6+5;
int n,m;
int dfn[N],low[N],tim,cnt;
vector<int> G[N],ans[N];
int stk[N],top;
void tarjan(int u,int pa)
{
dfn[u]=low[u]=++tim;
stk[++top]=u;
int son=0;
for(int v:G[u]){
if(!dfn[v]){
tarjan(v,u);
++son;
low[u]=min(low[u],low[v]);
if(pa==0&&son>1||low[v]>=dfn[u]){
++cnt;
while