很早就学过tarjan算法(割点割边与强联通)了,但是因为久不用老是忘,也有收藏过几篇不错的博客,但是每次需要时都要翻出那几篇太麻烦了,所以自己开篇记录方便自己的复习。图片和部分文字来源自其他博客,文末注明具体来源

首先是割点割边比较系统的定义

割点:无向连通图中,去掉一个顶点及和它相邻的所有边,图中的连通分量数增加,则该顶点称为割点。

桥(割边):无向联通图中,去掉一条边,图中的连通分量数增加,则这条边,称为桥或者割边。

割点与桥(割边)的关系

1)有割点不一定有桥,有桥一定存在割点

2)桥一定是割点依附的边。

下图中顶点C为割点,但和C相连的边都不是桥

当然我们是可以通过DFS暴力算法去找出割点和割边的,这里就不过多赘述,本文假设已有DFS算法基础,直接从DFS讲tarjan算法。

假设DFS中我们从顶点U访问到了顶点V(此时顶点V还未被访问过),那么我们称顶点U为顶点V的父顶点,V为U的孩子顶点。在顶点U之前被访问过的顶点,我们就称之为U的祖先顶点

显然如果顶点U的所有孩子顶点可以不通过父顶点U而访问到U的祖先顶点,那么说明此时去掉顶点U不影响图的连通性,U就不是割点。相反,如果顶点U至少存在一个孩子顶点,必须通过父顶点U才能访问到U的祖先顶点,那么去掉顶点U后,顶点U的祖先顶点和孩子顶点就不连通了,说明U是一个割点。

上图中的箭头表示DFS访问的顺序(而不表示有向图),对于顶点D而言,D的孩子顶点可以通过连通区域1红色的边回到D的祖先顶点C(此时C已被访问过),所以此时D不是割点。

上图中的连通区域2中的顶点,必须通过D才能访问到D的祖先顶点,所以说此时D为割点。再次强调一遍,箭头仅仅表示DFS的访问顺序,而不是表示该图是有向图。

这里我们还需要考虑一个特殊情况,就是DFS的根顶点(一般情况下是编号为0的顶点),因为根顶点没有祖先顶点。其实根顶点是不是割点也很好判断,如果从根顶点出发,一次DFS就能访问到所有的顶点,那么根顶点就不是割点。反之,如果回溯到根顶点后,还有未访问过的顶点,需要在邻接顶点上再次进行DFS,根顶点就是割点。

接下来谈一谈tarjan算法的具体实现细节

在具体实现Tarjan算法上,我们需要在DFS(深度优先遍历)中,额外定义三个数组dfn[],low[],parent[]

dfn数组

dnf数组的下标表示顶点的编号,数组中的值表示该顶点在DFS中的遍历顺序(或者说时间戳),每访问到一个未访问过的顶点,访问顺序的值(时间戳)就增加1。子顶点的dfn值一定比父顶点的dfn值大(但不一定恰好大1,比如父顶点有两个及两个以上分支的情况)。在访问一个顶点后,它的dfn的值就确定下来了,不会再改变。

 low数组

low数组的下标表示顶点的编号,数组中的值表示DFS中该顶点不通过父顶点能访问到的祖先顶点中最小的顺序值(或者说时间戳)。

每个顶点初始的low值和dfn值应该一样,在DFS中,我们根据情况不断更新low的值。

假设由顶点U访问到顶点V。当从顶点V回溯到顶点U时,

如果

dfn[v] < low[u]

那么

low[u] = dfn[v]

如果顶点U还有它分支,每个分支回溯时都进行上述操作,那么顶点low[u]就表示了不通过顶点U的父节点所能访问到的最早祖先节点。

parent数组

parent[]:下标表示顶点的编号,数组中的值表示该顶点的父顶点编号,它主要用于更新low值的时候排除父顶点,当然也可以其它的办法实现相同的功能。

接下来用一个具体的例子来观看tarjan算法的具体执行过程

下图中蓝色实线箭头表示已访问过的路径,无箭头虚线表示未访问路径。已访问过的顶点用黄色标记,未访问的顶点用白色标记,DFS当前正在处理的顶点用绿色表示。带箭头的蓝色虚线表示DFS回溯时的返回路径。

当DFS走到顶点H时,有三个分支,我们假设我们先走H-I,然后走H-F,最后走H-J。从H访问I时,顶点I未被访问过,所以I的dfn和low都为9。根据DFS的遍历顺序,我们应该从顶点I继续访问。

上图表示由顶点I访问顶点D,而此时发现D已被访问,当从D回溯到I时,由于

dfn[D] < dfn[I]

说明D是I的祖先顶点,所以到现在为止,顶点I不经过父顶点H能访问到的小时间戳为4。

根据DFS的原理,我们从顶点I回到顶点H,显然到目前为止顶点H能访问到的最小时间戳也是4(因为我们到现在为止只知道能从H可以通过I访问到D),所以low[H] = 4

现在我们继续执行DFS,走H-F路径,发现顶点F已被访问且dfn[F] < dfn[H],说明F是H的祖先顶点,但此时顶点H能访问的最早时间戳是4,而F的时间戳是6,依据low值定义low[H]仍然为4。

最后我们走H-J路径,顶点J未被访问过所以 dfn[J] = 10   low[J] = 10

同理,由DFS访问顶点B,dfn[J] > dfn[B],B为祖先顶点,顶点J不经过父顶点H能访问到的最早时间戳就是dfn[B],即low[J] = 2

我们从顶点J回溯到顶点H,显然到目前为止顶点H能访问到的最早时间戳就更新为2(因为我们到现在为止知道了能从H访问到J),所以low[H] = 2

根据DFS原理,我们从H回退到顶点E(H回退到G,G回退到F,F回退到E的过程省略),所经过的顶点都会更新low值,因为这些顶点不用通过自己的父顶点就可以和顶点B相连。当回溯到顶点E时,还有未访问过的顶点,那么继续进行E-K分支的DFS。

从E-K分支访问到顶点L时,顶点k和L的的dfn值和low值如图上图所示

接着我们继续回溯到了顶点D(中间过程有所省略),并更新low[D]

最后,按照DFS的原理,我们回退到顶点A,并且求出来了每个顶点的dfn值和low值。

tarjan算法求割点割边执行的全部过程如上图所示,现在我们拥有了全部顶点的dfn值和low值,可以开始判断是否是割点还是割边了

割点:判断顶点U是否为割点,用U顶点的dnf值和它的所有的孩子顶点的low值进行比较,如果存在至少一个孩子顶点V满足low[v] >= dnf[u],就说明顶点V访问顶点U的祖先顶点,必须通过顶点U,而不存在顶点V到顶点U祖先顶点的其它路径,所以顶点U就是一个割点。对于没有孩子顶点的顶点,显然不会是割点。

桥(割边):low[v] > dnf[u] 就说明V-U是桥

需要说明的是,Tarjan算法从图的任意顶点进行DFS都可以得出割点集和割边集。

从上图的结果中我们可以看出,顶点B,顶点E和顶点K为割点,A-B以及E-K和K-L为割边。

以上是tarjan算法求割点割边的理论,下面来看看具体的tarjan函数模板


void tarjan(int u,int fa)
{dfn[u]=low[u]=++index;for(int i=0;i<G[u].size();i++){int v=G[u][i];if(!dfn[v]){tarjan(v,u);low[u]=min(low[u],low[v]);}else if(v!=fa){low[u]=min(low[u],dfn[v]);}
}

上面这篇很简单的模板来自于我自己的POJ3352题解的部分内容,https://blog.csdn.net/qq_19895789/article/details/80076591

求出了图的dfn和low数组,相信无论判断割点还是割边都是相当容易的了,具体的看题目要求。

这里有两点要注意的,第一这里我没开parent(father)数组记录父节点,而是用函数传参的方式,部分题目时还是需要开数组方便统计。同样也可以用是否在栈中来判断,马上的强联通中会讲,具体看题目要求

第二,else if里的low[u]=min(low[u],dfn[v]),有的人可能会在强联通中也写作low[u]=min(low[u],low[v]),在强联通里是可以AC的,但是放在求割点割边会WA,这是为什么呢?下面是一点资料

关于tarjan算法,一直有一个很大的争议,就是low[u]=min(low[u],dfn[v]);
这句话,如果改成low[u]=min(low[u],low[v])就会wa掉,但是在求强连通分量时却没有问题
①根据许多大佬的观点,我想提出自己的一点看法,在求强连通分量时,如果v已经在栈中,那么说明u,v一定在同一个强连通分量中,所以到最后low[u]=low[v]是必然的,提前更新也不会有问题,但是在求割点时,low的定义有了小小的变化,不再是最早能追溯到的祖先,(因为是个无向图)没有意义,应该是最早能绕到的割点,为什么用绕到,是因为是无向边,所以有另一条路可以走,如果把dfn[v]改掉就会上翻过头,可能翻进另一个环中,所以wa掉(转自洛谷)
②low[u]=min(low[u],dfn[v])中的dfn[v]可不可以改成low[v]呢,答案是不可以,当你从u访问v到,发现v被访问过的时候,v的low值不一定确定了,说不定只有一个暂时的值而已,也就是:tmpdfn=lastdfn tmplow!=lastlow,所以不能代替。(转自Acfun栗主席)

上面的就是tarjan求割点和割边的概念和代码模板了,下面再来说一说tarjan求强联通的概念和代码模板。

首先看一看强联通的一些概念

在有向图G中,如果两个顶点间至少存在一条路径(注意是互相存在),称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。

下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。

上面如何计算dfn数组和low数组已经说过了,下面我们既然要计算强联通分量,肯定也要储存强联通分量

而为了存储整个强连通分量,这里挑选的容器是,栈。每次一个新节点出现,就进栈,如果这个点有 出度 就继续往下找。直到找到底,每次返回上来都看一看子节点与这个节点的LOW值,谁小就取谁,保证最小的子树根。如果找到DFN[]==LOW[]就说明这个节点是这个强连通分量的根节点(毕竟这个LOW[]值是这个强连通分量里最小的。)最后找到强连通分量的节点后,就将这个栈里,比此节点后进来的节点全部出栈,它们就组成一个全新的强连通分量。

下面给出一段伪代码参考一下

tarjan(u)
{DFN[u]=Low[u]=++Index                      // 为节点u设定次序编号和Low初值Stack.push(u)                              // 将节点u压入栈中for each (u, v) in E                       // 枚举每一条边if (v is not visted)               // 如果节点v未被访问过tarjan(v)                  // 继续向下找Low[u] = min(Low[u], Low[v])else if (v in S)                   // 如果节点v还在栈内Low[u] = min(Low[u], DFN[v])if (DFN[u] == Low[u])                      // 如果节点u是强连通分量的根repeatv = S.pop                  // 将v退栈,为该强连通分量中一个顶点print vuntil (u== v)
}

可以看到,这一段伪代码对比我上面求割点割边的代码就多了一个压栈和退栈储存强联通的过程(伪代码中为打印)

下面是对tarjan判断强联通算法的一个演示

从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。

返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。

返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。

继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。

至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。

最后呢附上我的HDOJ1269 迷宫城堡 AC代码,题目我就不贴了,有兴趣的可以自行搜索,大意就是判断该图是否强联通,如果是输出YES,如果不是输出NO,因为该题模板较为简单就不加注释了,如果有没看懂的可以返回去看伪代码的注释。最后这里是只判断了联通快数量,如果要输出每个联通块的节点可以再加一个数组储存(在弹栈时标记数组array[tmp]=cnt),具体看题目要求。

#include<iostream>
#include<cstdio>
#include<vector>
#include<stack>
#include<algorithm>
#include<cstring>
using namespace std;
vector<int> G[10005];
int dfn[10005]={0},low[10005]={0},ins[10005]={0};
stack<int> s;
int index=0,cnt=0;
void tarjan(int u)
{dfn[u]=low[u]=++index;s.push(u);ins[u]=1;for(int i=0;i<G[u].size();i++){int v=G[u][i];if(!dfn[v]){tarjan(v);low[u]=min(low[u],low[v]);}else if(ins[v])low[u]=min(low[u],dfn[v]);}if(low[u]==dfn[u]){cnt++;ins[u]=0;int tmp;do{tmp=s.top();s.pop();}while(tmp!=u);}
}
int main()
{int n,m;while(scanf("%d %d",&n,&m)!=EOF&&(n!=0||m!=0)){vector<int> tmpg[10005];swap(tmpg,G);memset(dfn,0,sizeof(int)*10005);memset(low,0,sizeof(int)*10005);index=cnt=0;for(int i=0;i<m;i++){int a,b;scanf("%d %d",&a,&b);G[a].push_back(b);}for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);printf("%s\n",cnt==1?"Yes":"No");}return 0;
}

参考内容:

https://www.byvoid.com/zhs/blog/scc-tarjan

https://blog.csdn.net/qq_34374664/article/details/77488976

https://www.cnblogs.com/nullzx/p/7968110.html

tarjan算法 割点割边强联通 算法讲解模板 自用整理相关推荐

  1. 强联通块tarjan算法

    http://poj.org/problem?id=1236 第一问:需要几个学校存在软件,才能通过传递,使得所有的学校都有软件 用tarjan算法求出强联通分量后,将每个联通分量缩成一个点,那么问题 ...

  2. POJ 2762Going from u to v or from v to u?(强联通 + 缩点 + 拓扑排序)

    [题意]: 有N个房间,M条有向边,问能否毫无顾虑的随机选两个点x, y,使从①x到达y,或者,②从y到达x,一定至少有一条成立.注意是或者,不是且. [思路]: 先考虑,x->y或者y-> ...

  3. Tarjan算法 (强联通分量 割点 割边)

    变量解释: low 指当前节点在同一强连通分量(或环)能回溯到的dfn最小的节点 dfn 指当前节点是第几个被搜到的节点(时间戳) sta 栈 vis 是否在栈中 ans 指强连通分量的数量 top ...

  4. POJ 2186 popular cow 有向图的强联通问题 Tarjan算法

    参考:http://hi.baidu.com/1093782566/blog/item/e5a0e9229913bd048b82a175.html http://www.cppblog.com/Iro ...

  5. 『Tarjan算法 无向图的双联通分量』

    无向图的双连通分量 定义:若一张无向连通图不存在割点,则称它为"点双连通图".若一张无向连通图不存在割边,则称它为"边双连通图". 无向图图的极大点双连通子图被 ...

  6. Tarjan算法超详细讲解(割点割边强连通)

    今天我主要介绍Tarjan算法在割点割边以及强连通分量中的应用以及缩点技巧 按照老规矩, 先上两道模板题 [模板]强连通分量 [模板]割点(割顶) 割点割边 一, 离散数学中的定义: 割点: 无向连通 ...

  7. Tarjan的强联通分量

    求强联通分量有很多种. <C++信息学奥赛一本通>  中讲过一个dfs求强联通分量的算法Kosdaraju,为了骗字数我就待会简单的说说.然而我们这篇文章的主体是Tarjan,所以我肯定说 ...

  8. tarjan求割点和桥(割边)模板

    tanjan算法相关概念 为了与有向图尽可能保持一致,我们将无向图的一条无向边拆分成两条单向边.两条边互为反向边. 从图中一点作为起点,进行DFS搜索遍历图,这样会得到一棵树,我们称之为DFS搜索树, ...

  9. tarjan求强联通分量

    [概述] Tarjan 算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树. 搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量. [ ...

最新文章

  1. Lazy Load, 延迟加载图片的 jQuery 插件
  2. java 成员变量的初始化_Java类变量和成员变量初始化过程
  3. Apache Shiro:简化应用程序安全性
  4. mysql三种引擎_MySQL常见的三种存储引擎
  5. Debian下使用OpenLDAP
  6. 【学习笔记】OSG 基本几何图元
  7. c语言程序设计 实验十一,C语言实验十一 结构体程序设计(二)
  8. Clojure 学习入门(3)- 数字类型
  9. asp.net ashx + JQuery Ajax + XML
  10. 【ember zigbee】第三章:ug103-02-fundamentals-zigbee 学习笔记(中)
  11. 帕斯卡计算机的控制原理,液体静力学基本方程式和帕斯卡原理
  12. cocos2d-x-3.4-025-仿霍比特人3五军之战片尾动画
  13. 槑图秀秀 (初学JAVA第三篇)
  14. 喜报丨内蒙古谱尼医学获批开展临床基因扩增检验技术
  15. tensorflow roadshow 全球巡回演讲 会议总结
  16. 剑指offer: 数组中数字出现的次数(曾在滴滴校招面试中遇到过)、leetcode消失的数字
  17. python xlrd 错误:xlrd.biffh.XLRDError: Unsupported format, or corrupt file: Expected BOF reco
  18. 服务器上自动备份数据库突然不备份了
  19. 《信息系统行锁等待的成因分析及智能化解决方案》
  20. 《“ 追梦人” 的逐梦路:探寻大学生创客群体的发展之道》

热门文章

  1. 写在一年半前的关于网站改版事宜
  2. Linux 发展编年表
  3. Windows 7系统盘合成
  4. 互联网企业蜂拥进入区块链,入局之前该思考什么?
  5. codeforces7D Palindrome Degree(manacheramp;dp或Hshamp;dp)
  6. 收集各种文章资料的URL 不断更新
  7. 内部类都有哪些?什么是内部类
  8. 完整优雅的卸载腾讯云云服务器安全监控组件
  9. UUOffice 工具箱,一款功能强大的 Excel 办公插件,好用推荐 ~
  10. 【产品】《用户体验要素》结构层(交互设计和信息架构)