一.概述.
tarjan算法是Robert tarjan发明的一种基于深度优先遍历的算法,这种算法可以求无向图的割点(割顶)割边(桥),进一步可以用于求无向图的双连通分量;在有向图方面可以求出强连通分量等.
二.割点与割边.
割点与割边是图论中重要的概念.
割点的定义:在一张无向图中,去掉一个点,可以将这个点所在的联通块分成两个或多个联通块,则称这个点是这张无向图的割点.
割边的定义:在一张无向图中,去掉一条边,可以将这条边所在的联通块分成两个或多个联通块,则称这个点是这张无向图的割边.
三.tarjan算法与割边.
关于tarjan算法的定义,本人也不是很清楚,可能就是一类用到了生成树或者dfn与low值的深度优先遍历算法吧.
运用tarjan算法求割点与割边首先要了解dfn与low值.
dfn值:dfn值其实就是时间戳,深度优先遍历时一个点i的dfn值就是一个点被遍历到的顺序(即被遍历到之前有几个节点被遍历过).
low值:
我们dfs遍历出来的是一棵生成树,在这棵生成树上的边我们称作树边,其它边我们称作非树边.
那么一个点的low值即为一个点通过由非树边组成的路径能够达到的dfn值最小的点的dfn值.
定理1:对于任意一条边,这条边连接的两点在生成树上一定是祖先与后代的关系.
证明如下:
若一条边连接的两点在生成树上不是祖先与后代,那么dfs是应该会从先dfs到的那个点dfs到另一个点,与假设相矛盾.
证毕.
那么我们现在就可以搬出定理2了.
定理2:若一条边是割边,则这条边只会出现在生成树上,且这条边连接的父亲的dfn值要大于儿子的low值.
证明如下:
若这条边是非树边,则去掉这条边之后,生成树一节连通,与割边的定义相矛盾.
若这条边的儿子的low值大于或等于了父亲的dfn值,根据low值的定义,去掉这条边后,这个儿子能够连通到的dfn值最小的点要小于它的父亲.
根据定理1,我们可以知道一条边连接的点的关系一定是祖先与后代,所以这个儿子可以连通到祖先,联通块没有被破坏,与割边的定义相矛盾.
证毕.
那么现在我们要做的就是在一个dfs内求出dfn值和low值,就可以得到割边了.
那么算法流程如下:
1.进行dfs,先确定这个点的dfn值.
2.遍历这个点的所有儿子.
3.每遍历一个儿子后,利用定理2判定这个儿子到它的边是否为割边,并打上标记.
4.若一个儿子没有被遍历过,则将这个点当前的low值与它儿子的low值取min.
5.若这个点被遍历过,则将这个点当前的low值与它儿子的dfn值取min.
那么求割边的模板如下:
void dfs(int k,int fa){
dfn[k]=low[k]=++tt;
for (int i=lin[k];i;i=e[i].next){
int y=e[i].y;
if (!dfn[y]) {
dfs(y,i);
low[k]=min(low[y],low[k]);
if (low[y]>dfn[k]) e[i].br=e[i^1].br=1;
}else if (i^fa^1){ //不能变成判y是否k的父亲,否则会被重边卡
low[k]=min(dfn[y],low[k]);
}
}
}
四.tarjan算法与割点.
接下来我们讨论割点.
定理3:一个割点若不是生成树的根,则这个点一定有一个儿子的low值小于等于这个点的dfn值.
证明如下:
设一个点为k.
若这个点k没有一个儿子满足这个条件,则所有儿子必定能不通过与点k之间的边通向k的父亲,这张图依旧联通,与割点的定义相矛盾.
证毕.
定理4:为根的节点为割点必须有两个儿子.
证明如下:
根据定理1,根节点的两棵子树一定不连通.
那么容易得出,若去掉根节点,根的所有子树都会分裂成单独的连通块.所以只要根有两个或两个以上的子树,就是割点.
证毕.
那么根据以上两条定理,我们就可以得出割点的算法流程:
1.进行dfs,将这个点k的dfn值确定,将low值初始为自己的dfn值.
2.遍历一遍它的所有儿子,同时更新low值.
3.判断这个点k是否是割点.
代码如下:
void dfs(int k){
dfn[k]=low[k]=++num;
int son=0;
for (int i=lin[k];i;i=e[i].next){
int y=e[i].y;
if (!dfn[y]){
dfs(y);
low[k]=min(low[k],low[y]);
if (low[y]>=dfn[k]){
son++;
if (k^root||son>1) cut[k]=1;
}
}else low[k]=min(low[k],dfn[y]);
}
}
五.一些细节.
自环可以不用处理,但是输入时还是建议把自环跳过.
求割点的时候可以不用判断是不是父亲,但求割边必须判断.
若图不连通还需要将所有点跑一遍.