连通性问题,这可真是tarjan的天下啊,不过这篇文章并没有打算扯到tarjan的起源模型强连通分量,主要还是说说自己对其它连通性问题的思考,所以,如果你还不会tarjan算法的话,嗯,点这里:byvoid的tarjan算法讲解 膜拜一下神牛。
当然了,关于连通性问题这里还有:byvoid的连通性问题讲解 再次膜拜。
1、连通:两个点之间存在若干条边将其连接,称其连通
2、强连通:有向图中的两点可以互达(A→B 并且 B→A),称其强连通19、横叉边:在DFS中连接当前点与已访问完毕的点之间的边
20、后向边:就是在DFS中,子孙指向祖先的边。
双连通分量有两种:点双连通分量、边双连通分量。那双连通分量又是什么?到底是点的还是边的?这样不清楚的表述屡见不鲜,参考了众多人的博客后,关于双连通分量的定义,还是确定不下来,主要有以下几种说法:
1、指点双连通,与块同义
横叉边是一个定义在有向图搜索树中的概念,对于无向图它是没有任何意义的。有向图出现横叉边的原因是u→v不可行,然后u已经退栈成功,然而v→u可行,所以会访问到已经退栈的节点,这样的边称之为横叉边,然而在无向图中,这样的情况是不可能出现的,如果u→v是可行的,那么v→u也是可行的,因为无向图中的边是没有方向的,那么,在求无向图相关的桥、割点、点双连通分量、边双连通分量的时候,就不需要开一个布尔数组来记录该点是否访问完毕,即是否还在栈中,并且,也不需要开布尔数组来记录该点是否已经访问过,因为访问过的点dfn <> 0,据此可知,在无向图的连通求解中,可以不开任何布尔数组,切记切记!!
[有向图强连通分量]
在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。
下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
[Tarjan算法]
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或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) // 如果节点v还在栈内
Low[u] = min(Low[u], DFN[v])
if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
repeat
v = S.pop // 将v退栈,为该强连通分量中一个顶点
print v
until (u== v)
}
接下来是对算法流程的演示。
从节点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}。
可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。
针对tarjan的操作规则来讲解这个算法
其实,tarjan算法的基础是DFS。我们准备两个数组Low和Dfn。Low数组是一个标记数组,记录该点所在的强连通子图所在搜索子树的根节点的Dfn值(很绕嘴,往下看你就会明白),Dfn数组记录搜索到该点的时间,也就是第几个搜索这个点的。根据以下几条规则,经过搜索遍历该图(无需回溯)和对栈的操作,我们就可以得到该有向图的强连通分量。
由于每个顶点只访问过一次,每条边也只访问过一次,我们就可以在O(n+m)的时间内求出有向图的强连通分量。但是,这么做的原因是什么呢?
Tarjan算法的操作原理如下:
求有向图的强连通分量还有一个强有力的算法,为Kosaraju算法。Kosaraju是基于对有向图及其逆图两次DFS的方法,其时间复杂度也是O(N+M)。与Trajan算法相比,Kosaraju算法可能会稍微更直观一些。但是Tarjan只用对原图进行一次DFS,不用建立逆图,更简洁。在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。
求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量的Tarjan算法,以及求最近公共祖先的离线Tarjan算法,在此对Tarjan表示崇高的敬意。
#include "cstdlib"
#include "cctype"
#include "cstring"
#include "cstdio"
#include "cmath"
#include "algorithm"
#include "vector"
#include "string"
#include "iostream"
#include "sstream"
#include "set"
#include "queue"
#include "stack"
#include "fstream"
//#include "strstream"
using namespace std;
#define M 2000 //题目中可能的最大点数
int STACK[M],top=0; //Tarjan 算法中的栈
bool InStack[M]; //检查是否在栈中
int DFN[M]; //深度优先搜索访问次序
int Low[M]; //能追溯到的最早的次序
int ComponetNumber=0; //有向图强连通分量个数
int Index=0; //索引号
vector Edge[M]; //邻接表表示
vector Component[M]; //获得强连通分量结果
void Tarjan(int i)
{
int j;
DFN[i]=Low[i]=Index++;
InStack[i]=true;
STACK[++top]=i;
for (int e=0;e
// virtualDestruction.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include
#include
#include
#include
//#include <>
using namespace std;
#define M 2000
vector Edge[M];//邻接表
vector res[M];//强连通分量
int componentNum=0;//强连通分量个数
int dfn[M];//每个点的遍历次序
int index=0;//次序索引
int low[M];//该点所在强连通分量所在搜索树的根节点次序号
stack seq;//遍历的点
bool inStack[M];
void tarjan(int i)
{
dfn[i]=low[i]=index++;
seq.push(i);
inStack[i]=true;
for (int j=0;jlow
}
if (dfn[i]==low[i])
{
int k=i;
componentNum++;
do
{
inStack[i]=false;
k=seq.top();
res[componentNum-1].push_back(k);
seq.pop();
}while(i!=k);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
int N=6;
Edge[0].push_back(2);Edge[0].push_back(1);
Edge[1].push_back(3);
Edge[2].push_back(4);Edge[2].push_back(3);
Edge[3].push_back(0);Edge[3].push_back(5);
Edge[4].push_back(5);
memset(dfn,-1,sizeof(dfn));
memset(low,-1,sizeof(low));
memset(inStack,0,sizeof(inStack));
for(int i=0;i
#include
#include
#include
//从顶点0开始
// 要用的话要初始化:调用Adj.initial 和 tarjan.initial
//要解决问题用调用tarjan.solve
//对tarjan.initial要传入的参数是图边集Adj,和顶点个数n
const int maxn = 11000;
//顶点的规模
const int maxm = 210000;
//边的规模,如果是无向图要记得乘以2
const int GRAY = 0;
const int WHITE =-1;
const int BLACK = 1;
typedef struct Edge{
int s;
int e;
int next;
}Edge;
typedef struct Adj{
int edge_sum;
int head[maxn];
Edge edge[maxm];
void initial(){
edge_sum = 0;
memset(head,-1,sizeof(head));
}
void add_edge(int a, int b){
edge[edge_sum].s = a;
edge[edge_sum].e = b;
edge[edge_sum].next = head[a];
head[a] = edge_sum++;
}
}Adj;
typedef struct Tanjan{
int n;
int *head;
Adj *adj;
Edge *edge;
int cnt;
int top;
int cur;
int dfn[maxn];
int low[maxn];
int color[maxn];
int stack[maxn];
int belong[maxn];
void initial(Adj *_adj,int _n){
n = _n;
adj = _adj;
head = (*adj).head;
edge = (*adj).edge;
}
void solve(){
memset(dfn,-1,sizeof(dfn));
memset(color,WHITE,sizeof(color));
top = cnt = cur = 0;
for(int i = 0; i < n; i++)
if(color[i] == WHITE)//找到一个白色的顶点,就开始处理
tarjan(i);
}
inline int min(int a, int b){
if(a < b) return a;
else return b;
}
void tarjan(int i){
int j = head[i];
color[i] = GRAY;//标记为灰色
stack[top++] = i;//把结点圧入栈顶
dfn[i] = low[i] = ++cur;//给结点一个时间戳,并给Low初始化
while(j != -1){
int u = edge[j].e;
if (dfn[u] == WHITE){
tarjan(u);
low[i] = min(low[i],low[u]);
//更新low
}else if (color[u] == GRAY)
low[i] = min(low[i],dfn[u]);
//一条后向边
j = edge[j].next;
}
color[i] = BLACK;
if(low[i] == dfn[i]){
do{
j = stack[--top];
belong[j] = cnt;
}while(i != j);
++cnt;
}
}
}Tarjan;
Adj adj;
Tarjan tj;
http://lib.csdn.net/article/datastructure/10310
http://www.cppblog.com/sosi/archive/2010/09/26/127797.aspx
http://blog.csdn.net/nothi/article/details/7739741
http://blog.csdn.net/e6894853/article/details/7898185
http://blog.csdn.net/xinghongduo/article/details/6195337