Tarjan算法 求 有向图的强连通分量
百度百科
https://baike.baidu.com/item/tarjan%E7%AE%97%E6%B3%95/10687825?fr=aladdin
参考博文
http://blog.csdn.net/qq_34374664/article/details/77488976
http://blog.csdn.net/mengxiang000000/article/details/51672725
https://www.cnblogs.com/shadowland/p/5872257.html
算法介绍(基于DFS)
了解tarjan算法之前你需要知道:强连通,强连通图,强连通分量。
强 连 通:如果两个顶点可以相互通达,则称两个顶点强连通(strongly connected)。
在一个有向图G里,设两个点a和b, 发现由a有一条路可以走到b,由b又有一条 路可以走到a,我们就叫这两个顶点(a,b)强连通。(注意间接连接也可以)
强连通图:如果有向图G的每两个顶点都强连通,称G是一个强连通图。
强连通分量:在一个有向图G中,有一个子图G2,这个子图G2每2个点都满足强连通,我们就 把这个子图G2叫做强连通分量 。
我们来看一个有向图方便理解:
标注蓝色线条框框的部分就是一个强连通分量,节点3也是一个强连通分量
我们再来看一个图(百度百科中的图):
子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量实际上为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入到一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
在Tarjan算法中,有如下定义。
(注意,下面的定义如看不明白没关系,多看后面的模拟就明白了)
DFN[u]数组 : 在DFS中该节点被搜索的次序编号,每个点的次序编号都不一样。
通俗地解释DFN[u]: 意思就是在DFS的过程中,当前的这个节点是第几个被遍历到的点
LOW[u]数组 : u或u的子树能够追溯到的最早的栈中节点的次序号
通俗地解释LOW[u]: 就是在DFS的过程中,如果当前节点是极大强联通子图的话,他的根节点的标号就是对应的LOW值:
当DFN[ u ]==LOW[ u ]时,u或u的子树可以构成一个强连通分量。
通俗地解释:当DFN[ u ]==LOW[ u ]时,以u为根的搜索子树上所有节点是一个强连通分量。
回溯条件: DFS遇到的节点在已在栈中或者DFS遇到无出度的节点时就回溯。
回溯时需要维护LOW[u]的值。
如果下一个要遍历的节点在栈中,那么就要把当前节点的LOW[u]值设置成下一个节点(在栈中)的DFN[v]值。如:LOW[u]=DFN[v] 或者LOW[u]= min(LOW[u], DFN[v])
如果还需要接着回溯,那么接着维护LOW[u]=min(LOW[u],LOW[v])
(即使v搜过了也要进行这步操作,但是v一定要在栈内才行)
u代表当前节点,v代表其能到达的节点。在进行一层深搜之后回溯的时候,维护LOW[u]的值。如果我们发现了某个节点回溯之后维护的LOW[u]值还是==DFN[u]的值,那么这个节点无疑就是一个关键节点:
算法演示:以1为Tarjan 算法的起始点,如图:前面不明白没关系,重点从这里开始看
假如从1号节点开始遍历,开始dfs,并不断设置当前节点的DFN值和LOW值,并把当前这个节点压入栈中,那么第一次在节点6处停止,因为6没有出度。
那么此时的DFN和LOW值分别为:
从1开始: DFN[1]=LOW[1]= ++index ----1
入栈 1
由1进入3: DFN[3]=LOW[3]= ++index ----2
入栈 1 3
由3进入5: DFN[5]=LOW[5]= ++index ----3
入栈 1 3 5
由5进入6: DFN[6]=LOW[6]= ++index ----4
入栈 1 3 5 6
可以用下图来表示:
因为节点6无出度,于是判断 DFN[6]==LOW[6],把6出栈(pop)。
{6}是一个强连通分量。
目前栈的节点有: 1 3 5 见下图:
之后回溯到节点5,节点6被访问过了并出栈(pop)了,所以它也没有能访问的边了,
那么 DFN[5]==LOW[5],{5} 也是一个强连通分量,弹出5
目前栈的节点有: 1 3
返回节点3,继续搜索到节点4,节点4是新节点,设DFN[4]=LOW[4]=5并把4加入堆栈。
DFN[4]=LOW[4]= ++index -----5
入栈 1 3 4 见下图:
继续节点4往下找,找到了节点1 。
因为1号节点还在栈中,那么就代表着栈中的现有的所有元素构成了一个强连通图
(仔细想想、、兜了一圈又回到起点1)
回溯到节点4,更新 LOW[4]的值: LOW[4]= min(LOW[4], DFN[1]) 值更新为1
再接着访问4的下一个节点6,节点6 被访问过并POP了,就不用管它了。
再回溯到节点3,更新 LOW[3]的值: LOW[3]= min(LOW[3], LOW[4]) 值更新为1
3号节点也没有能访问的下一个节点了。图如下:
再回溯到节点1,更新 LOW[1]的值: LOW[1]= min(LOW[1], LOW[3]) 值还是为1
节点1还有边没有走过。发现节点2,访问节点2,节点2是新节点,放入栈中
DFN[2]=LOW[2]=++index ----6
入栈 1 3 4 2
由节点2,走到4,发现4被访问过了,4还在栈里,
回溯到节点2 更新LOW[2] = min(LOW[2], DFN[4]) LOW[2]=5
节点2也没有可访问的下一个节点了。
再回溯到节点1 更新LOW[1] = min(LOW[1], LOW[2]) LOW[1]=1
这时我们发现LOW[1]==DFN[1] 说明以1为 根节点 的强连通分量已经找完了。
将栈中1,3,4,2全部节点都出栈。{1,3,4,2} 是强连通分量。图如下
至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2}, {5}, {6}。
可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。
实战:(后面有Tarjan算法的伪代码及模板,可以参考)
P1726 上白泽慧音 https://www.luogu.org/problemnew/show/1726
P2661 信息传递 https://www.luogu.org/problemnew/show/2661
P3379 LCA Tarjan算法 https://www.luogu.org/problemnew/show/3379
P1262 间谍网络 (提示:可用Tanjan缩点) https://www.luogu.org/problemnew/show/1262
P3387 【模板】缩点 https://www.luogu.org/problemnew/show/3387
以下4道题是北京大学的是英文题,如不明白意思可看下面的翻译:
题解 http://blog.csdn.net/u012469987/article/details/51292558#poj-1236-network-of-schools
http://poj.org/problem?id=2186 http://poj.org/problem?id=1236
http://poj.org/problem?id=1904 http://poj.org/problem?id=1330
接下来我们讨论一下Tarjan算法另外能够干一些什么:
既然我们知道,Tarjan算法相当于在一个有向图中找有向环,那么我们Tarjan算法最直接的能力就是缩点辣!缩点基于一种染色实现,我们在Dfs的过程中,尝试把属于同一个强连通分量的点都染成一个颜色,那么同一个颜色的点,就相当于一个点。
比如刚才的实例图中缩点之后就可以变成这样:
将一个有向带环图变成了一个有向无环图(DAG图)。很多算法要基于有向无环图才能进行的算法就需要使用Tarjan算法实现染色缩点,建一个DAG图然后再进行算法处理。在这种场合,Tarjan算法就有了很大的用武之地!
那么这个时候 ,我们再引入一个数组color【i】表示节点i的颜色,再引入一个数组stack【】实现一个栈,然后在Dfs过程中每一次遇到点都将点入栈,在每一次遇到关键点的时候将栈内元素弹出,一直弹到栈顶元素是关键点的时候为止,对这些弹出来的元素进行染色即可。
缩点代码实现:
void Tarjan(int u) //此代码仅供参考
{
vis[u]=1;
low[u]=dfn[u]=cnt++;
stack[++tt]=u;
for(int i=0;i<mp[u].size();i++)
{
int v=mp[u][i];
if(vis[v]==0)Tarjan(v);
if(vis[v]==1)low[u]=min(low[u],low[v]);
}
if(dfn[u]==low[u])
{
sig++;
do
{
low[stack[tt]]=sig;
color[stack[tt]]=sig;
vis[stack[tt]]=-1;
}
while(stack[tt--]!=u);
}
}
Tarjan算法伪代码参考:
//注意,v指的是u能达到的下一个节点
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) // 如果节点u还在栈内
Low[u] = min(Low[u], DFN[v])
if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
repeat v = S.pop // 将v退栈,为该强连通分量中一个顶点
print v
until (u== v)
}
Tanjan算法模板:
void Tarjan ( int x )
{
dfn[ x ] = ++dfs_num ;
low[ x ] = dfs_num ;
vis [ x ] = true ; //是否在栈中
stack [ ++top ] = x ;
for ( int i=head[ x ] ; i!=0 ; i=e[i].next )
{
int temp = e[ i ].to ;
if ( !dfn[ temp ] )
{
Tarjan ( temp ) ;
low[ x ] = gmin ( low[ x ] , low[ temp ] ) ;
}
else if ( vis[ temp ])low[ x ] = gmin ( low[ x ] , dfn[ temp ] ) ;
}
if ( dfn[ x ]==low[ x ] ) //构成强连通分量
{
vis[ x ] = false ;
color[ x ] = ++col_num ; //染色
while ( stack[ top ] != x ) //清空
{
color [stack[ top ]] = col_num ;
vis [ stack[ top-- ] ] = false ;
}
top -- ;
}
}
Tanjan算法另一个模板:
#define M 5010 //题目中可能的最大点数
int STACK[M],top=0; //Tarjan算法中的栈
bool InStack[M]; //检查是否在栈中
int DFN[M]; //深度优先搜索访问次序
int Low[M]; //能追溯到的最早的次序
int ComponentNumber=0; //有向图强连通分量个数
int Index=0; //索引号
vector<int> Edge[M]; //邻接表表示
vector<int> Component[M]; //获得强连通分量结果
int InComponent[M]; //记录每个点在第几号强连通分量里
int ComponentDegree[M]; //记录每个强连通分量的度
void Tarjan(int i)
{
int j;
DFN[i]=Low[i]=Index++;
InStack[i]=true;STACK[++top]=i;
for (int e=0;e<Edge[i].size();e++)
{
j=Edge[i][e];
if (DFN[j]==-1)
{
Tarjan(j);
Low[i]=min(Low[i],Low[j]);
}
else
if (InStack[j]) Low[i]=min(Low[i],DFN[j]);
}
if (DFN[i]==Low[i])
{
ComponentNumber++;
do{
j=STACK[top--];
InStack[j]=false;
Component[ComponentNumber].
push_back(j);
InComponent[j]=ComponentNumber;
}
while (j!=i);
}
}
Tarjan算法裸代码:
输入:
一个图有向图。
输出:
它每个强连通分量。
这个图就是刚才讲的那个图。一模一样。
input:
6 8
1 3
1 2
2 4
3 4
3 5
4 6
4 1
5 6
output:
6
5
3 4 2 1
#include<cstdio>
#include<algorithm>
#include<string.h>
using namespace std;
struct node
{
int v,next;
}edge[1001];
int DFN[1001],LOW[1001];
int stack[1001],heads[1001],visit[1001],cnt,tot,index;
void add(int x,int y)
{
edge[++cnt].next=heads[x];
edge[cnt].v = y;
heads[x]=cnt;
return ;
}
void tarjan(int x) //代表第几个点在处理。递归的是点。
{
DFN[x]=LOW[x]=++tot; //新进点的初始化。
stack[++index]=x; //进栈
visit[x]=1; //表示在栈里
for(int i=heads[x];i!=-1;i=edge[i].next)
{
if(!DFN[edge[i].v]) //如果没访问过
{
tarjan(edge[i].v); //往下进行延伸,开始递归
LOW[x]=min(LOW[x],LOW[edge[i].v]);//递归出来,比较谁是谁的儿子/父亲,就是树的对应关系,涉及到强连通分量子树最小根的事情。
}
else if(visit[edge[i].v ])//如果访问过,并且还在栈里
{
LOW[x]=min(LOW[x],DFN[edge[i].v]);//比较谁是谁的儿子/父亲。就是链接对应关系
}
}
if(LOW[x]==DFN[x]) //发现是整个强连通分量子树里的最小根。
{
do{
printf("%d ",stack[index]);
visit[stack[index]]=0;
index--;
}while(x!=stack[index+1]);//出栈,并且输出。
printf("\n");
}
return ;
}
int main()
{
memset(heads,-1,sizeof(heads));
int n,m;
scanf("%d%d",&n,&m);
int x,y;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1;i<=n;i++)
if(!DFN[i]) tarjan(i);//当这个点没有访问过,就从此点开始。防止图没走完
return 0;
}
Tarjan算法 求 有向图的强连通分量的更多相关文章
- Tarjan算法求有向图的强连通分量
算法描述 tarjan算法思想:从一个点开始,进行深度优先遍历,同时记录到达该点的时间(dfn记录到达i点的时间),和该点能直接或间接到达的点中的最早的时间(low[i]记录这个值,其中low的初始值 ...
- Tarjan算法初探 (1):Tarjan如何求有向图的强连通分量
在此大概讲一下初学Tarjan算法的领悟( QwQ) Tarjan算法 是图论的非常经典的算法 可以用来寻找有向图中的强连通分量 与此同时也可以通过寻找图中的强连通分量来进行缩点 首先给出强连通分量的 ...
- Tarjan算法求有向图强连通分量并缩点
// Tarjan算法求有向图强连通分量并缩点 #include<iostream> #include<cstdio> #include<cstring> #inc ...
- 图论-求有向图的强连通分量(Kosaraju算法)
求有向图的强连通分量 Kosaraju算法可以求出有向图中的强连通分量个数,并且对分属于不同强连通分量的点进行标记. (1) 第一次对图G进行DFS遍历,并在遍历过程中,记录每一个点的退出顺序 ...
- (转)求有向图的强连通分量个数(kosaraju算法)
有向图的连通分量的求解思路 kosaraju算法 逛了很多博客,感觉都很难懂,终于找到一篇能看懂的,摘要记录一下 原博客https://www.cnblogs.com/nullzx/p/6437926 ...
- 【数据结构】DFS求有向图的强连通分量
用十字链表结构写的,根据数据结构书上的描述和自己的理解实现.但理解的不透彻,所以不知道有没有错误.但实验了几个都ok. #include <iostream> #include <v ...
- 求有向图的强连通分量个数 之 Kosaraju算法
代码: #include<cstdio> #include<cstring> #include<iostream> using namespace std; ][] ...
- HDU 1269 迷宫城堡 tarjan算法求强连通分量
基础模板题,应用tarjan算法求有向图的强连通分量,tarjan在此处的实现方法为:使用栈储存已经访问过的点,当访问的点离开dfs的时候,判断这个点的low值是否等于它的出生日期dfn值,如果相等, ...
- tarjan算法-解决有向图中求强连通分量的利器
小引 看到这个名词-tarjan,大家首先想到的肯定是又是一个以外国人名字命名的算法.说实话真的是很佩服那些算法大牛们,佩服得简直是五体投地啊.今天就遇到一道与求解有向图中强连通分量的问题,我的思路就 ...
随机推荐
- 集合Collection ----List集合
Collection集合体系的特点: set系列集合:添加的元素是 无序,不重复,无索引的 ----HashSet: 无序,不重复,无索引 ----LinkedHashSet: 有序,不重复,无索引 ...
- JavaScript进行表单提交
表单结构,设置form表单的id属性,method="post/get","action"要跳转的页面(jsp或servlet) <form name=& ...
- http请求在https中使用
问题原因:HTTPS页面里动态的引入HTTP资源,比如引入一个js文件,会被直接block掉的.在HTTPS页面里通过AJAX的方式请求HTTP资源,也会被直接block掉的. 解决方案: <m ...
- js判断苹果端,安卓端
<script type="text/javascript"> var browser = { versions : function() { var u = navi ...
- three.js 元素跟随物体效果
需求: 1.实现元素跟随指定物体位置进行位置变化 实现方案: 1--- Sprite 精灵 2 --- cavans 画图后创建模型贴图 3 --- CSS2DRenderer渲染方式 4 --- ...
- 将可执行程序设为linux服务启动
将可执行程序设为linux服务启动 如何将我们自己的程序设为linux的一个服务程序,并实现开机启动,需要经过如下三步: 1 把可执行程序放在一个linux系统可以找到的地方. ...
- P6628-[省选联考 2020 B 卷] 丁香之路【欧拉回路,最小生成树】
正题 题目链接:https://www.luogu.com.cn/problem/P6628 题目大意 给出\(n\)个点的一张完全无向图,\(i\sim j\)的边权是\(|i-j|\). 然后给出 ...
- P4980-[模板]Pólya定理
正题 题目链接:https://www.luogu.com.cn/problem/P4980 题目大意 \(n\)个物品图上\(m\)种颜色,求在可以旋转的情况下本质不同的涂色方案. 解题思路 既然是 ...
- Python:安装opencv出现错误Could not find a version that satisfies the requirement numpy==1.13.3 (from versions: 1.14.5, 1.14.6, 1.15.0rc2, 1.15.0, 1.15.1, 1.15.2, 1.15.3, 1.15.4, 1.16.0rc1, 1.16.0rc2,
安装opencv的时候,出现numpy的版本不匹配,卸载了不匹配的版本,重新安装却是一点用都没有,后面尝试了一下这里的提示pip更新,居然安装成功了,看来pip的版本过低真是误事啊. 报错是: Cou ...
- VS2013的主函数问题
报错如下: 打开属性里面,修改字符集即可