简介
Tarjan主要用来求强连通分量的,假如说对于一个有向图G,这个图中每一个点都能够互相到达,那么G就是一个强连通图。对于一个有向图中的极大强连通子图就是强连通分量。(即有向图中的一个环就是一个强连通分量)
如图,一种颜色的点就是一个强连通分量。
算法步骤
我们将有向图变成一棵搜索树。如下图(和上图不一样)。
如图,蓝色的为返祖边,红色的为横叉边。
我们对于每个点设DFN[i]为遍历到这个点的时间戳,LOW[i]为遍历到的最早的时间戳。假如说搜到了一个返祖边(如边(7,3)),证明3,6,7三点可以互相到达,所以{3,6,7}为强连通分量。
所以我们开一个栈来储存环中的点,如果找到强连通分量,就要将强连通分量中的所有点退掉。
假如遇到了横叉边(i,j),假如栈内有点j,那么就要更新LOW[i]的值,否则不更新。
当DFN[i]=LOW[i]的时候,证明栈顶到当前点这一段都是属于当前点所属的这个环的,将这个环中的所有点退掉。
Code
图1
void tarjan(int x)
{
h++;
dfn[x]=low[x]=h;
bz[x]=1;
st[++top]=x;
int i,now;
for (i=head[x];i;i=next[i])
{
now=go[i];
if (dfn[now]==-1)
{
tarjan(now);
low[x]=min(low[x],low[now]);
} else
if (bz[now]) low[x]=min(low[x],dfn[now]);
}
if (dfn[x]==low[x])
{
cnt++;
while (st[top+1]!=x)
{
bz[st[top]]=0;
g[st[top]]=cnt;
top--;
}
}
}
全家桶
在后面两年的OI学习过程中,我对tarjan又有了新的理解。
tarjan需要非常熟练,否则做tarjan题的时候十分吃亏。
接下来便介绍一下tarjan的其他经典操作。
点双与边双
无向图中:
点双定义:去掉点双中任意一点都不会改变该图的连通性。
边双定义:去掉边双中任意一边都不会改变该图的连通性。
点双性质:任意两个点之间都可找到两条点不重复的路径,可以理解为若干个有点相交的环。割点,如果去掉它,这个图就不连通了。
边双性质:任意两个点之间都可找到两条边不重复的路径,可以理解为若干个有边相交的环。桥,如果去掉它,这个图就不连通了。
判断点是否在栈中?
只有在有向图中才需要判断。
判断点是否在栈中,实质上是判断有向图中的横叉边。而无向图中没有横叉边,所以不用bool数组判断。
求割点及桥
利用dfn序和low来求得。
割点:如果dfs树上存在一条边(x,y),其中x是y的dfs树的父亲,使得
l
o
w
[
y
]
>
=
d
f
n
[
x
]
low[y]>=dfn[x]
low[y]>=dfn[x],那么x必为割点。
dfs树的根z是否为割点?如果它有多个儿子,且存在一对儿子的low不同,那么z为割点。
桥:如果dfs树上存在一条边(x,y),其中x是y的dfs树的父亲,使得
l
o
w
[
y
]
>
=
d
f
n
[
y
]
(
即
l
o
w
[
y
]
=
d
f
n
[
y
]
)
low[y]>=dfn[y](即low[y]=dfn[y])
low[y]>=dfn[y](即low[y]=dfn[y]),则(x,y)为桥。
那么非dfs树的边呢?一定不是桥。
无向图中,边只有树边和非树边,而非树边必为返祖边,那么必然产生环。
求点双和边双
求点双中的点
当x不为dfs树根时:
如果
l
o
w
[
y
]
>
=
d
f
n
[
x
]
low[y]>=dfn[x]
low[y]>=dfn[x],则x必为割点,栈中的点一定在点双中。直接弹栈即可。
当x为dfs树根时:
x不一定为割点,但栈中的点一定在点双中。直接弹栈即可。
综上所述,直接弹栈即可。
图2
void tarjan(int x){
int i;
Dfn[x]=Low[x]=++T;tar[T]=x;
st[++st[0]]=x;
for(i=head[x];i;i=edge[i].next)
if(!Dfn[edge[i].to]){
tarjan(edge[i].to);
v=edge[i].to;
Low[x]=min(Low[x],Low[v]);
if(Low[v]>=Dfn[x]){
CNT++;
while(v^st[st[0]+1]){
Bz[st[st[0]]]=0;
st[0]--;
}
}
}else
Low[x]=min(Low[x],Dfn[edge[i].to]);
}
求边双中的点
如果
l
o
w
[
y
]
>
=
d
f
n
[
x
]
low[y]>=dfn[x]
low[y]>=dfn[x],那么直接弹栈即可。
当然了,像图1的代码一样也行。
图3
void tarjan(int x,int y){
int i;
dfn[x]=low[x]=++T;
st[++top]=x;
for(i=head[x];i;i=edge[i].next){
if(edge[i].id==y)continue;
if(!dfn[edge[i].to]){
tarjan(edge[i].to,edge[i].id);
low[x]=Min(low[x],low[edge[i].to]);
}else low[x]=Min(low[x],dfn[edge[i].to]);
}
if(dfn[x]==low[x]){
CNT++;
while(st[top+1]^x){
bel[st[top]]=CNT;
top--;
}
}
}
缩点双和边双之后
维护点双和边双的信息。我们希望将图转化为树。
点双
圆方树。
圆点代表原图中的点,方点代表点双。
每个方点连向该点双中的点,最终会成为一棵树。
如果根深度为1且为圆点,那么奇数深度的点为圆点,偶数深度的点为方点。
方点与圆点的信息密切相关。
边双
缩掉边双之后,直接就成为了一棵树。
维护树上的信息即可。
例题
JZOJ5909
POJ3352