Tarjan 算法
一.算法简介
Tarjan 算法一种由Robert Tarjan提出的求解有向图强连通分量的算法,它能做到线性时间的复杂度。
我们定义:
如果两个顶点可以相互通达,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connectedcomponents)。
例如:在上图中,{1 , 2 , 3 , 4 } , { 5 } , { 6 } 三个区域可以相互连通,称为这个图的强连通分量。
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
再Tarjan算法中,有如下定义。
DFN[ i ] : 在DFS中该节点被搜索的次序(时间戳)
LOW[ i ] : 为i或i的子树能够追溯到的最早的栈中节点的次序号
当DFN[ i ]==LOW[ i ]时,为i或i的子树可以构成一个强连通分量。
二.算法图示
以1为Tarjan 算法的起始点,如图
顺次DFS搜到节点6
回溯时发现LOW[ 5 ]==DFN[ 5 ] , LOW[ 6]==DFN[ 6 ] ,则{ 5 } , { 6 } 为两个强连通分量。回溯至3节点,拓展节点4.
拓展节点1 , 发现1再栈中更新LOW[ 4 ],LOW[ 3 ] 的值为1
回溯节点1,拓展节点2
自此,Tarjan Algorithm 结束,{1 , 2 , 3 , 4 } , { 5 } , { 6 } 为图中的三个强连通分量。
不难发现,Tarjan Algorithm 的时间复杂度为O(E+V).
三.算法模板
1void Tarjan ( int x ) {
2 dfn[ x ] = ++dfs_num ;
3 low[ x ] = dfs_num ;
4 vis [ x ] = true ;//是否在栈中
5 stack [ ++top ] = x ;
6for ( int i=head[ x ] ; i!=0 ;i=e[i].next ){
7int temp = e[ i ].to ;
8if ( !dfn[ temp ] ){
9 Tarjan (temp ) ;
10 low[ x ] = gmin (low[ x ] , low[ temp ] ) ;
11 }
12elseif ( vis[ temp ])low[ x ] = gmin ( low[ x ] , dfn[ temp ] );
13 }
14if ( dfn[ x ]==low[ x ] ) {//构成强连通分量
15 vis[ x ] = false ;
16 color[ x ] = ++col_num ;//染色
17while ( stack[ top ] != x ) {//清空
18 color [stack[ top]] = col_num ;
19 vis [ stack[ top--] ] = false ;
20 }
21 top -- ;
22 }
23 }
http://blog.jobbole.com/79314/
Tarjan算法详解
http://blog.csdn.net/jeryjeryjery/article/details/52829142?locationNum=4&fps=1#
在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。
如下图中,强连通分量有:{1,2,3,4},{5},{6}
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。Tarjan算法有点类似于基于后序的深度遍历搜索和并查集的组合,充分利用回溯来解决问题。
在Tarjan算法中为每个节点i维护了以下几个变量:
DFN[i]:深度优先搜索遍历时节点i被搜索的次序。
low[i]:节点i能够回溯到的最早位于栈中的节点。
flag[i]:标记几点i是否在栈中。
Tarjan算法的运行过程:
1.首先就是按照深度优先搜索算法搜索的次序对图中所有的节点进行搜索。
2.在搜索过程中,对于任意节点u和与其相连的节点v,根据节点v是否在栈中来进行不同的操作:
*节点v不在栈中,即节点v还没有被访问过,则继续对v进行深度搜索。
*节点v已经在栈中,即已经被访问过,则判断节点v的DFN值和节点u的low值的大小来更新节点u的low值。如果节点v的 DFN值要小于节点u的low值,根据low值的定义(能够回溯到的最早的已经在栈中的节点),我们需要用DFN值来更新u 的low值。
3.在回溯过程中,对于任意节点u与其子节点v(其实不能算是子节点,只是在深度遍历的过程中,v是在u之后紧挨着u的节点)的 low值来更新节点u的low值。因为节点v能够回溯到的已经在栈中的节点,节点u也一定能够回溯到。因为存在从u到v的直接路 径,所以v能够到的节点u也一定能够到。
4.对于一个连通图,我们很容易想到,在该连通图中有且仅有一个节点u的DFN值和low值相等。该节点一定是在深度遍历的过 程中,该连通图中第一个被访问过的节点,因为它的DFN值和low值最小,不会被该连通图中的其他节点所影响。下面我们证 明为什么仅有一个节点的DFN和low值相等。假设有两个节点的DFN值和low值相等,由于这两个节点的DFN值一定不相同 (DFN值的定义就是深度遍历时被访问的先后
次序),所以两个的low值也绝对不相等。由于位于同一个连通图中,所以两个节点必定相互可达,那么两者的low值一定会 被另外一个所影响(要看谁的low值更小),所以不可能存在两对DFN值和low值相等的节点。
所以我们在回溯的过程中就能够通过判断节点的low值和DFN值是否相等来判断是否已经找到一个子连通图。由于该连通图中 的DFN值和low值相等的节点是该连通图中第一个被访问到的节点,又根据栈的特性,则该节点在最里面。所以能够通过不停 的弹栈,直到弹出该DFN值和low值相同的节点来弹出该连通图中所有的节点。
Tarjan算法的C++实现代码如下,可以配合上面的图加以理解:
[cpp] view plain copy
#include<iostream>
using namespace std;
int DFN[105]; //记录在做dfs时节点的搜索次序
int low[105]; //记录节点能够找到的最先访问的祖先的记号
int count=1; //标记访问次序,时间戳
int stack[105]; //压入栈中 int top=-1;
int flag[105]; //标记节点是否已经在栈中
int number=0;
int j;
int matrix[105][105]={{0,1,1,0,0,0},{0,0,0,1,0,0},{0,0,0,1,1,0},{1,0,0,0,0,1},{0,0,0,0,0,1},{0,0,0,0,0,0}};
int length; //图的长度
1. void tarjan(int u){
2. DFN[u]=low[u]=count++; //初始化两个值,自己为能找到的最先访问的祖先
3. stack[++top]=u;
flag[u]=1; //标记为已经在栈中
for(int v=0;v<length;v++){
if(matrix[u][v]){
if(!DFN[v]){ //如果点i没有被访问过
tarjan(v); //递归访问
4. if(low[v]<low[u])
5. low[u]=low[v]; //更新能找的到祖先
6. }
7. else{ //如果访问过了,并且该点的DFN更小,则
8. if(DFN[v]<low[u]&&flag[v]) //flag[v]这个判断条件很重要,这样可以避免已经确定在其他联通图的v,因为u到v的单向边而影响到u的low
9. low[u]=DFN[v]; //也就是已经确定了的联通图要剔除掉,剔除的办法就是判断其还在栈中,因为已经确定了的连通图的点
10. } //flag在下面的do while中已经设为0了(即已经从栈中剔除了)
11. }
12. }
13.
14. //往后回溯的时候,如果发现DFN和low相同的节点,就可以把这个节点之后的节点全部弹栈,构成连通图
15. if(DFN[u]==low[u]){
16. number++; //记录连通图的数量
17. do{
18. j=stack[top--]; //依次取出,直到u
19. cout<<j<<" ";
20. flag[j]=0; //设置为不在栈中
21. }while(j!=u);
22. cout<<endl;
23. }
24. }
25. int main(){
26.
27. memset(DFN,0,sizeof(DFN)); //数据的初始化
28. memset(low,0,sizeof(low));
29. memset(flag,0,sizeof(flag));
30.
31. length=6;
32. tarjan(0);
33.
34. cout<<endl;
35. for(int i=0;i<6;i++){
36. cout<<"DFN["<<i<<"]:"<<DFN[i]<<" low["<<i<<"]:"<<low[i]<<endl;
}
37. return 0;
38. }
http://blog.csdn.net/xinghongduo/article/details/6195337
1、求有向图的强连通分量
如果有向图G中的任何两个顶点都相互可达,则G称为一个强连通图。非强连通图的极大强连通子图称为有向图的强连通分量。
Tarjan算法是根据图的深度优先搜索,定义DFN(u)为顶点u在DFS中的次序编号,Low(u)为u或u的子树能够追溯到的最早的栈中顶点的次序编号,则
Low(u)=min{DFN(u),Low(v) if(u,v)是树枝边,DFN(v) if(u,v)是后向边}
当DFN(u)=Low(u)时,以u为根的搜索子树上所有顶点都一个强连通分支。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | #include <iostream> #include <stdio.h> #include <stack> using namespace std;
struct Edge { int adj_vertex; Edge* next; };
struct Vertex { int vertex; Edge* head; };
struct Graph { Vertex vertices[50]; int vertex_num; };
int Visited[50]; int inStack[50]; int DFN[50]; int Low[50]; int time=0; stack<int> S;
void Create_Graph(Graph *g) { cout <<"enter the number of vertices:"; cin >>g->vertex_num;
for(int i=1;i<=g->vertex_num;++i) { g->vertices[i].vertex=i; g->vertices[i].head=NULL; }
for(int i=1;i<=g->vertex_num;++i) { cin.clear(); cout <<"enter vertex "<<i<<"'s edges:"; int vtx; Edge* temp; while(cin >>vtx) { temp=new Edge; temp->adj_vertex=vtx; temp->next=g->vertices[i].head; g->vertices[i].head=temp; } } }
void Tarjan(int u,const Graph *g) { ++time; DFN[u]=Low[u]=time; Visited[u]=1; S.push(u); inStack[u]=1; for(Edge* temp=g->vertices[u].head;temp;temp=temp->next) { int v=temp->adj_vertex; if(Visited[v]==0) { Tarjan(v,g); if(Low[u]>Low[v]) Low[u]=Low[v]; } else if(inStack[v] && Low[u]>DFN[v]) Low[u]=DFN[v]; }
if(DFN[u]==Low[u]) { int vtx; cout <<"set is:"; do { vtx=S.top(); S.pop(); inStack[vtx]=0; cout <<vtx<<" "; } while (vtx!=u); } }
int main() { Graph* g=new Graph; Create_Graph(g); for(int i=1;i<=g->vertex_num;++i) { Visited[i]=0; inStack[i]=0; DFN[i]=0; Low[i]=0; } for(int i=1;i<=g->vertex_num;++i) if(Visited[i]==0) Tarjan(i,g); } |
2、求割点
割点:在图G中删除一个顶点u及其相关的边后,图G的连通分支数增加。
定义DFN(u)为顶点u在DFS中的次序编号,Low(u)为u或u的搜索树子树中能通过非父子边追溯到的次序编号最小的顶点。
Low(u)=min{DFS(u),Low(v) if(u,v)是树枝边,DFS(v) if(u,v)是后向边}
一个顶点u是割点,当且仅当满足(1)或(2)
(1)u为树根,且u有多于一个子树
(2)u不为树根,且满足存在(u,v)为树枝边,使得DFS(u)<=Low(v)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | #include <iostream> #include <stdio.h> #include <stack> using namespace std;
struct Edge { int adj_vertex; Edge* next; };
struct Vertex { int vertex; Edge* head; };
struct Graph { Vertex vertices[50]; int vertex_num; };
int Visited[50]; int inStack[50]; int DFN[50]; int Low[50]; int time=0; stack<int> S;
void Create_Graph(Graph *g) { cout <<"enter the number of vertices:"; cin >>g->vertex_num;
for(int i=1;i<=g->vertex_num;++i) { g->vertices[i].vertex=i; g->vertices[i].head=NULL; }
for(int i=1;i<=g->vertex_num;++i) { cin.clear(); cout <<"enter vertex "<<i<<"'s edges:"; int vtx; Edge* temp; while(cin >>vtx) { temp=new Edge; temp->adj_vertex=vtx; temp->next=g->vertices[i].head; g->vertices[i].head=temp; } } }
void Tarjan(int u,const Graph *g) { ++time; DFN[u]=Low[u]=time; Visited[u]=1;
for(Edge* temp=g->vertices[u].head;temp;temp=temp->next) { int v=temp->adj_vertex; if(Visited[v]==0) { Tarjan(v,g); if(Low[u]>Low[v]) Low[u]=Low[v]; } else if(Low[u]>DFN[v]) Low[u]=DFN[v]; }
if(DFN[u]==1 && g->vertices[u].head && g->vertices[u].head->next) cout <<"cutting point is "<<u<<endl; else if(DFN[u]>1) { for(Edge* temp=g->vertices[u].head;temp;temp=temp->next) { int v=temp->adj_vertex; if(DFN[u]<=Low[v]) { cout <<"cutting point is "<<u<<endl; break; } } } }
int main() { Graph* g=new Graph; Create_Graph(g); for(int i=1;i<=g->vertex_num;++i) { Visited[i]=0; inStack[i]=0; DFN[i]=0; Low[i]=0; } for(int i=1;i<=g->vertex_num;++i) if(Visited[i]==0) Tarjan(i,g); } |
Tarjan算法求解一个无向图中的割点和桥问题
基本概念
割点:Articulation Point
在无向连通图中,删除一个顶点v及其相连的边后,原图从一个连通分量变成了两个或多个连通分量,则称顶点v为割点,同时也称关节点(Articulation Point)。
双连通的图:一个没有关节点的连通图称为重连通图(biconnectedgraph)(双连通图)。
连通度:k,若在连通图上至少删去k 个顶点才能破坏图的连通性。
算法应用
算法应用:通信网络中,用来衡量系统可靠性,连通度越高,可靠性越高。
求解方法
1. 暴力求解,依次删除每一个节点v,用DFS(或者BFS)判断是否连通,再把节点加入图中。若用邻接表(adjacency list),需要做V次DFS,时间复杂度为O(V∗(V+E))O(V∗(V+E))
2. Jarjan算法,只用一次DFS求解。
第一个方法不多说:
对于第二个方法
我们要维持两个数据结构:
dfn[u]:记录节点u在DFS过程中被遍历到的次序号。
low[u]:记录节点u或u的子树通过非父子边追溯到最早的祖先节点(即DFS次序号最小)。
其中对于low[u]的理解是这样的:当(u,v)为树边时,且v没有被访问过,则low[u]是min(low[u]和low[v])。当v被访问过,如果v不是u的父节点(如果是则说明有重边,不考虑),则(u,v)为回边,则low[u]取min(low[u], dfn[v])。
代码思路解析
#include<iostream>
#include<vector>
#include<fstream>
usingnamespacestd;
#define N 201
vector<int>G[N];
bool visit[N];
int dfn[N];
int low[N];
int parent[N];
intmin(
inta,
intb)
{
if(a < b)
returna;
elsereturn b;
}
void input()
{
int n, m;
//分别表示顶点数和边数
ifstream in(
"input.txt");
in >> n >> m;
int a, b;
for(
inti =
1; i <= m; ++i)
{
in >> a >> b;
G[a].push_back(b);
/*邻接表储存无向边*/
G[b].push_back(a);
}
}
voiddfs(
intu)
{
//记录dfs遍历次序
staticintcounter =
0;
//记录节点u的子树数
intchildren =
0;
visit[u] =
true;
//初始化dfn与low
dfn[u] = low[u] = ++counter;
for(
intj =
0; j < G[u].size(); j++)
//遍历与u相连的顶点
{
int v = G[u][j];
if (!visit[v])
{
children++;
parent[v] = u;
//u是v的父节点
dfs(v);
//深度优先搜索v
low[u] = min(low[u], low[v]);
//等v完成深度优先搜索之后,low[u]记录节点u或u的子树通过非父子边追溯到最早的祖先节点(即DFS次序号最小)
if(parent[u] == -
1&& children >
1)
//对根节点u,若其有两棵或两棵以上的子树,则该根结点u为割点;
{
cout<<
"1 articulation point: "<< u<<endl;
}
if(parent[u] != -
1&& low[v] >= dfn[u])
//对非叶子节点u(非根节点),若其子树的节点均没有指向u的祖先节点的回边(条件low[v] >= dfn[u]表达的就是),说明删除u之后,根结点与u的子树的节点不再连通;则节点u为割点。
{
cout<<
"2 articulation point: "<< u << endl;
//这样做,可能出现某个顶点多次输出,其实可以用一个指示变量来指示,做到顶点不重复输出
}
if (low[v] >dfn[u])
//桥的条件
{
cout<<
"Bridge "<< v <<
" "<< u << endl;
}
}
elseif (v != parent[u]) {
//节点v已访问,则(u,v)为回边,且不是重边
low[u] = min(low[u], dfn[v]);
}
}
}
int main()
{
/*input();
memset(dfn, -1, sizeof(dfn));
memset(father, 0, sizeof(father));
memset(low, -1, sizeof(low));
memset(is_cut, false, sizeof(is_cut));
count();*/
memset(dfn, -
1,
sizeof(dfn));
memset(low, -
1,
sizeof(low));
memset(visit,
false,
sizeof(visit));
memset(parent, -
1,
sizeof(parent));
input();
dfs(
1);
system(
"pause");
return0;
}
参考
两个很好的讲解很详细的参考资料:
http://www.cnblogs.com/en-heng/p/4002658.html
http://www.cnblogs.com/c1299401227/p/5402747.html
http://www.cnblogs.com/c1299401227/p/5402747.html
tarjan算法--求无向图的割点和桥
一.基本概念
1.桥:是存在于无向图中的这样的一条边,如果去掉这一条边,那么整张无向图会分为两部分,这样的一条边称为桥无向连通图中,如果删除某边后,图变成不连通,则称该边为桥。
2.割点:无向连通图中,如果删除某点后,图变成不连通,则称该点为割点。
二:tarjan算法在求桥和割点中的应用
1.割点:1)当前节点为树根的时候,条件是“要有多余一棵子树”(如果这有一颗子树,去掉这个点也没有影响,如果有两颗子树,去掉这点,两颗子树就不连通了。)
2)当前节点U不是树根的时候,条件是“low[v]>=dfn[u]”,也就是在u之后遍历的点,能够向上翻,最多到u,如果能翻到u的上方,那就有环了,去掉u之后,图仍然连通。
保证v向上最多翻到u才可以
2.桥:若是一条无向边(u,v)是桥,
1)当且仅当无向边(u,v)是树枝边的时候,需要满足dfn(u)<low(v),也就是v向上翻不到u及其以上的点,那么u--v之间一定能够有1条或者多条边不能删去,因为他们之间有一部分无环,是桥。
如果v能上翻到u那么u--v就是一个环,删除其中一条路径后,能然是连通的。
3.注意点:
1)求桥的时候:因为边是无方向的,所以父亲孩子节点的关系需要自己规定一下,
在tarjan的过程中if(v不是u的父节点) low[u]=min(low[u],dfn[v]);
因为如果v是u的父亲,那么这条无向边就被误认为是环了。
2)找桥的时候:注意看看有没有重边,有重边的边一定不是桥,也要避免误判。
4.也可以先进行tarjan(),求出每一个点的dfn和low,并记录dfs过程中的每个点的父节点,遍历所有点的low,dfn来寻找桥和割点
三:求桥和割点的模板:
#include<iostream>
usingnamespace std;
#include<cstdio>
#include<cstring>
#include<vector>
#define N 201
vector<int>G[N];
int n,m,low[N],dfn[N];
bool is_cut[N];
int father[N];
int tim=0;
void input()
{
scanf("%d%d",&n,&m);
int a,b;
for(int i=1;i<=m;++i)
{
scanf("%d%d",&a,&b);
G[a].push_back(b);/*邻接表储存无向边*/
G[b].push_back(a);
}
}
void Tarjan(int i,int Father)
{
father[i]=Father;/*记录每一个点的父亲*/
dfn[i]=low[i]=tim++;
for(int j=0;j<G[i].size();++j)
{
int k=G[i][j];
if(dfn[k]==-1)
{
Tarjan(k,i);
low[i]=min(low[i],low[k]);
}
elseif(Father!=k)/*假如k是i的父亲的话,那么这就是无向边中的重边,有重边那么一定不是桥*/
low[i]=min(low[i],dfn[k]);//dfn[k]可能!=low[k],所以不能用low[k]代替dfn[k],否则会上翻过头了。
}
}
void count()
{
int rootson=0;
Tarjan(1,0);
for(int i=2;i<=n;++i)
{
int v=father[i];
if(v==1)
rootson++;/*统计根节点子树的个数,根节点的子树个数>=2,就是割点*/
else{
if(low[i]>=dfn[v])/*割点的条件*/
is_cut[v]=true;
}
}
if(rootson>1)
is_cut[1]=true;
for(int i=1;i<=n;++i)
if(is_cut[i])
printf("%d\n",i);
for(int i=1;i<=n;++i)
{
int v=father[i];
if(v>0&&low[i]>dfn[v])/*桥的条件*/
printf("%d,%d\n",v,i);
}
}
int main()
{
input();
memset(dfn,-1,sizeof(dfn));
memset(father,0,sizeof(father));
memset(low,-1,sizeof(low));
memset(is_cut,false,sizeof(is_cut));
count();
return0;
}