Tarjan算法
wu_zi_jin0917 · · 个人记录
Tarjan算法
写在前面
本篇笔记介绍Tarjan算法在求强连通分量(有向图)和求割点与桥以及双连通分量(无向图)中的运用。
前置知识:DFS树
DFS是在一张图中从任意结点出发按DFS的顺序建成的树,并记录每个结点
如图所示,这是一棵DFS树
显然,DFS树中不能包含所有的边,除了DFS树中的树边以外,还有以下几种边:
-
返祖边:返回当前结点的祖先的边
-
横叉边:通往不是当前结点祖先的已访问的边
-
前向边:通往不是当前结点祖先的未访问的边
强联通分量
定义: 强联通分量(SCC)是指在一张有向图中,满足任何结点互相均可达的极大的子图
在有向图中,可以通过Tarjan算法在
Tarjan算法中通过维护每个结点的
结点
如图所示,以图中的树为例,每个结点的
我们使用栈维护每一个SCC(处于当前SCC的结点都在栈中),将处理完的SCC中的结点出栈(这点下文会讲解决方法)
对于
记当前结点
-
若
v 未被访问过,则边(u,v) 是树边(例如图中5 \to 6 ),low[u]=min(low[u],low[v]) ,u 可能通过v 回溯到更早的结点 -
若
v 已被访问且仍在栈中(不在栈中的结点已经处理完了),则该边为非树边(例如图中6 \to 4 ),low[u]=min(low[u],dfn[v])
进一步思考,我们可以发现,对于一个结点
这是因为满足该条件的结点
所以,若是在一次回溯完成后,发现满足
由于栈先进后出的性质,此时栈中在结点
找出SCC的代码如下:
inline void Tarjan(int x)
{
dfn[x]=low[x]=++now,stk[++top]=x,fl[x]=1;//fl[]标记结点是否在栈中
for(int i=H1[x];i;i=G1[i].next)
{
int v=G1[i].v;
if(!dfn[v]) Tarjan(v),low[x]=min(low[v],low[x]);//情况1
else if(fl[v]) low[x]=min(low[x],dfn[v]);//情况2
}
if(dfn[x]==low[x])
{
cnt++;//SCC个数加1
while(1)
{
scc[stk[top]]=cnt;//标记栈中结点属于当前SCC
siz[cnt]++;//siz[i]:第i个SCC的大小,不断累加
fl[stk[top]]=0;
top--;
if(stk[top+1]==x) break;//当x被弹出时结束
}
}
}
应用:缩点
通过对有向图中的SCC的处理,我们可以将整张图进行缩点
缩点就是将图中的SCC缩成一个点(因为其中的点互相可达),来将原图变成一张DAG(有向无环图),以进行拓扑排序等操作
具体进行缩点操作时,对于原图中的一条边
代码如下:
for(int i=1;i<=tot1;i++)
if(scc[G1[i].u]!=scc[G1[i].v])
{
int x=scc[G1[i].u],y=scc[G1[i].v];
Add2(x,y);
}
习题:【模板】缩点
割点与桥
通过Tarjan算法在无向图中的运用,我们还可以求出无向图中所有的割点与桥
割点: 删除当前点使得无向图的连通块个数增加,则该点被称为割点
桥: 删除当前边使得无向图的连通块个数增加,则该点被称为桥(割边)
对于一个点
在DFS树中,若是想要从
同理,对于一条
需要注意的是,无向图中的DFS树是有向的,即不走回头路。
同时,对于DFS树的树根需要特判,若是
求割点的代码如下,求桥同理:
inline void Tarjan(int x)
{
dfn[x]=low[x]=++tot;
for(int i=0;i<G[x].size();i++)
{
int v=G[x][i];
if(!dfn[v])
{
Tarjan(v);
if(x==root) son++;
else
{
low[x]=min(low[x],low[v]);
if(low[v]>=dfn[x]) cut[x]=1;//标记割点
}
}
else low[x]=min(low[x],dfn[v]);
}
if(x==root && son>1) cut[x]=1;//特判树根
}
【模板】割点
双连通分量
处理了无向图中的割点与桥,我们可以求出无向图中的每一个双连通分量
双连通分量包括点-双连通分量(以下简称点双) 和 边-双连通分量(以下简称边双)
点-双连通分量: 一个满足每个结点与其他所有结点至少有2条点不重复的路径的连通分量
边-双连通分量: 一个满足每个结点与其他所有结点至少有2条边不重复的路径的连通分量
以下分别介绍两种双连通分量的求解方法。
点-双连通分量
有一个重要的结论:点双内部没有任何割点
若是有割点,割点两端的结点的路径必然经过割点,不符合定义。
与求SCC的方法类似,使用栈维护DFS时经过的结点,若是发现结点
代码如下:
inline void Tarjan(int x,int fa)
{
low[x]=dfn[x]=++tot,stk[++top]=x;
int son=0;
for(int i=H[x];i;i=G[i].next)
{
int v=G[i].v;
if(!dfn[v])
{
son++;
Tarjan(v,x);
low[x]=min(low[x],low[v]);
if(low[v]>=dfn[x])//若x为割点
{
cnt++;
while(stk[top+1]!=v) bcc[cnt].push_back(stk[top--]);//不断加入当前点双
bcc[cnt].push_back(x);//加入x
}
}
else if(v!=fa) low[x]=min(low[x],dfn[v]);
}
if(fa==0 && son==0) bcc[++cnt].push_back(x);//特判孤立点,自己就是点双
}
边-双连通分量
边双与点双类似,有结论:边双内没有桥
结论的证明一样,此处不加赘述
于是我们可以朴素的想:通过Tarjan算法找出所有的桥后,将这些桥删除,剩下的连通块就一定是边双了
求出桥的代码如下:
//此处有一个trick,将每条边从2开始编号
//那么设当前边编号为i,无向图中i的反边为i^1
inline void Tarjan(int x,int fa)//fa为连接x的上一条边
{
dfn[x]=low[x]=++amt;
for(int i=H[x];i;i=G[i].next)
{
int v=G[i].v;
if(!dfn[v])
{
Tarjan(v,i);
low[x]=min(low[v],low[x]);
if(dfn[x]<low[v]) cut[i]=cut[i^1]=1;//标记桥
}
else if(i!=(fa^1)) low[x]=min(low[x],dfn[v]);
}
}
标记桥之后,求出每个连通块的代码如下:
inline void Search(int x,int id)//id为结点x所属的边双
{
belong[x]=id,bcc[id-1].push_back(x);//加入当前结点
for(int i=H[x];i;i=G[i].next)
{
int v=G[i].v;
if(cut[i] || belong[v]) continue;//桥不能走,通往的结点未被处理
Search(v,id);
}
}
【模板】点双连通分量
【模板】边双连通分量