一:kosaraju算法
第一步:
对图进行后续遍历,用数组num记录每个节点访问的编号。(这里的后续遍历能够使得根节点的num值最大,从而保证比节点num值小的那些节点,根节点都可以访问到。)
第二步:
将原图边的方向倒转,然后从num值大的节点开始深度优先搜索。能够搜到的未删除的点的集合就是一个强连通子图。想象一下,如果第二步是从森林里选择树,那么哪个树是不连通(对于GT来说)到其他树上的呢?就是最后遍历出来的树,它的根节点在步骤1的遍历中离开时间最晚,而且可知它也是该树中离开时间最晚的那个节点。这给我们提供了很好的选择,在第一次深搜遍历时,记录时间i离开的顶点j,即numb[i]=j。那么,我们每次只需找到没有找过的顶点中具有最晚离开时间的顶点直接深搜(对于GT来说)就可以了。每次深搜都得到一个强连通分量。
隐藏性质:
分析到这里,我们已经知道怎么求强连通分量了。但是,大家有没有注意到我们在第二次深搜选择树的顺序有一个特点呢?如果在看上述思路的时候,你的脑子在思考,相信你已经知道了!!!它就是:如果我们把求出来的每个强连通分量收缩成一个点,并且用求出每个强连通分量的顺序来标记收缩后的节点,那么这个顺序其实就是强连通分量收缩成点后形成的有向无环图的拓扑序列。为什么呢?首先,应该明确搜索后的图一定是有向无环图呢?废话,如果还有环,那么环上的顶点对应的所有原来图上的顶点构成一个强连通分量,而不是构成环上那么多点对应的独自的强连通分量了。然后就是为什么是拓扑序列,我们在改进分析的时候,不是先选的树不会连通到其他树上(对于反图GT来说),也就是后选的树没有连通到先选的树,也即先出现的强连通分量收缩的点只能指向后出现的强连通分量收缩的点。那么拓扑序列不是理所当然的吗?这就是Kosaraju算法的一个隐藏性质。
证明如下:(转载)
命题1:该算法求出的都是强连通分量
假设在后来转置后的图中从xdfs到了 y处,说明存在路径x->y。因为这是在转置图中,所以说明原图中存在路径y->x。
然后另外一个信息就是x的序号在y之后。这有两种可能:
1、以y为根先DFS出了一棵搜索树(可以认为是整个搜索树的一棵子树),但是这棵子树里不包含x,并且此时x还未被dfs到。(利用反证法,如果这棵子树里包含了x,那么x的序号会在y之前)
2、y是x扩展出来的搜索树中的一个结点。
综合两个条件,综合两个条件取交。那么上面两种可能中的第一种不成立。因为存在路径y->x,所以如果x未被dfs到,一定会被y为根的搜索树包含的。于是只剩下第二种可能,那么第二种情况表明存在路径x->y。所以x,y可以互相到达。至此证明了该算法求出的都是强连通分量。命题1得证。
命题2:所有的极大强连通分量都会被该算法求到。
命题2等价于:存在两个点(i,j),他们互相可达,但是没有被放进同一个强连通分量中。易知若(i,j)可以互相到达,那么肯定其中一个点在另外一个点扩展出去的搜索树中(注意DFS的性质,走完一个分叉再走另外一个分叉)。
由于轮换对称,不妨设j在i扩展出去的搜索树中,那么显然j比i先出栈。假如命题2不成立,那么必须是有一棵以A为根(而且这个A必须比i后出栈)的搜索树,它包含了j但不包含i。由命题1可知,(A,j)可互达。那么由于(i,j)可互达,在这颗搜索树中,肯定能有j扩展到i,所以这样的一棵搜索树是不存在的。因此反证得命题2成立。
该算法比Tarjan算法慢一点,但是有一个好处:该算法依次求出的强连通分量已经是拓扑序的。
下面给出这一性质的证明:
对于两个不同的强连通分量A,B,设A中出栈顺序最晚的点为a,B中出栈顺序最晚的点为b。不妨设a出栈顺序在b之前,那么有两种可能。
1、存在路径b->a。由于两点不属于同一强连通分量,所以不存在路径a->b。这种情况下Kosaraju算法会先把B强连通分量拿出来,所以是满足拓扑序的。
2、不存在路径b->a。那么这种情况下必然也不存在路径a->b,否则a出栈之时,b必然已经出栈了。所以,先拿出B强连通分量是符合拓扑序的。
所以,该性质得证。
#include<iostream> #include<stdio.h> #include<string.h> using namespace std; bool map[100][100];//记录图形的边 bool visited[100];//记录点是否是被访问过了。 int dfn[100];//记录点深度优先搜索的顺序 int dotn;//记录点的个数 void init()//初始化 { cin>>dotn; int line; cin>>line; for(int i=1;i<=line;i++) { int u,v; cin>>u>>v; map[u][v]=1; } memset(visited,0,sizeof(visited)); } void dfsfirst(int u,int &time)//第一次深度优先搜索记录点的dfn值 { for(int i=1;i<=dotn;i++) { if(!visited[i]&&map[u][i]) { visited[i]=1; dfsfirst(i,time); } } dfn[++time]=u; } void dfssecond(int u)//对图的反向图进行深搜这时候只需要判断map[dfn[i]][u]就可以了因为如果此时有边就代表了方向图中有这条边 { for(int i=1;i<=dotn;i++) { if(!visited[dfn[i]]&&map[dfn[i]][u]) { visited[dfn[i]]=1; cout<<dfn[i]<<" "; dfssecond(dfn[i]); } } } void kosaraju() { int time=0; for(int i=1;i<=dotn;i++) { if(!visited[i]) { visited[i]=1; dfsfirst(i,time); } } memset(visited,0,sizeof(visited)); for(int i=dotn;i>=1;i--) { if(!visited[dfn[i]]) { visited[dfn[i]]=1; cout<<dfn[i]<<" "; dfssecond(dfn[i]); cout<<endl; } } } int main() { init(); kosaraju(); return 0; }
二、 Trajan算法1. 算法思路:对图进行深度优先搜索,再搜索过程中用dfn记录搜索的顺序。用backn记录在搜索过程中的点所能到达的未删除的顺序次最小(不一定是最小的但是肯定比正在搜索的点小)的点(包括回溯到达的)。用栈来记录已经搜索过的但是未删除的点。当dfn[u]==back[u]将栈中u之前的点输出就是最大强连通分量。
2.代码:#include<iostream> #include<stdio.h> #include<string.h> #include<stack> using namespace std; bool visited[100];//记录点是否被访问过 bool del[100];//记录点是否被删除 bool map[100][100];//记录图形 int dfn[100];//记录每个点访问的次序 int backn[100];//记录每个点回溯时候到达的次小点 int dotn;//点的数量 stack<int>s; void init()//初始化 { int line; cin>>dotn; cin>>line; for(int i=1;i<=line;i++) { int u,v; cin>>u>>v; map[u][v]=1; } memset(visited,0,sizeof(visited)); memset(del,0,sizeof(del)); } void dfs(int u,int &time) { dfn[u]=++time; backn[u]=time; for(int i=1;i<=dotn;i++) { if(map[u][i]) { if(!visited[i]) { visited[i]=1; s.push(i); dfs(i,time); backn[u]=min(backn[u],backn[i]); } else { if(!del[i]) { backn[u]=min(backn[u],dfn[i]);//这里注意是dfn[i]不能是backn[i]因为backn·[i]储存的点可能是一已经删除的点。 } } } } if(backn[u]==dfn[u]) { while(s.top()!=u) { cout<<s.top()<<" "; del[s.top()]=1; s.pop(); } cout<<u<<endl; del[u]=1; s.pop(); } } void Tarjan() { init(); int time=0; for(int i=1;i<=dotn;i++) { if(!visited[i]) { s.push(i); visited[i]=1; dfs(i,time); } } } int main() { freopen("in.txt","r",stdin); Tarjan(); return 0; }
三、Gabow算法
1. 思路分析
这个算法其实就是Tarjan算法的变异体,我们观察一下,只是它用第二个堆栈来辅助求出强连通分量的根,而不是Tarjan算法里面的dfn[]和backn[]数组。那么,我们说一下如何使用第二个堆栈来辅助求出强连通分量的根。
我们使用类比方法,在Tarjan算法中,每次backn[i]的修改都是由于环的出现(不然,backn[i]的值不可能变小),每次出现环,在这个环里面只剩下一个backnk[i]没有被改变(深度最低的那个),或者全部被改变,因为那个深度最低的节点在另一个环内。那么Gabow算法中的第二堆栈变化就是删除构成环的节点,只剩深度最低的节点,或者全部删除,这个过程是通过出栈来实现,因为深度最低的那个顶点一定比前面的先访问,那么只要出栈一直到栈顶那个顶点的访问时间不大于深度最低的那个顶点。其中每个被弹出的节点属于同一个强连通分量。那有人会问:为什么弹出的都是同一个强连通分量?因为在这个节点访问之前,能够构成强连通分量的那些节点已经被弹出了,这个对Tarjan算法有了解的都应该清楚,那么Tarjan算法中的判断根我们用什么来代替呢?想想,其实就是看看第二个堆栈的顶元素是不是当前顶点就可以了。
现在,你应该明白其实Tarjan算法和Gabow算法其实是同一个思想的不同实现,但是,Gabow算法更精妙,时间更少(不用频繁更新backn[])。#include<iostream> #include<stdio.h> #include<string.h> #include<stack> using namespace std; bool map[100][100];//记录图形 bool visited[100];//记录点是不是已经被访问过了 bool del[100];//记录点是不是已经删除了 int dfn[100];//记录点访问的次序 stack<int>s1,s2; int dotn; void init() { int line; cin>>dotn>>line; for(int i=1;i<=line;i++) { int u,v; cin>>u>>v; map[u][v]=1; } memset(visited,0,sizeof(visited)); memset(del,0,sizeof(del)); } void dfs(int u,int &time) { visited[u]=1; dfn[u]=++time; s1.push(u); s2.push(u); for(int i=1;i<=dotn;i++) { if(map[u][i]) { if(!visited[i]) { dfs(i,time); } else { if(!del[i]) { while(dfn[s2.top()]>dfn[i])s2.pop();//注意这个地方 } } } } if(u==s2.top()) { while(u!=s1.top()) { cout<<s1.top()<<" "; del[s1.top()]=1; s1.pop(); } cout<<u<<endl; del[s1.top()]=1; s1.pop(); s2.pop(); } } void Gadow() { init(); int time=0; for(int i=1;i<=dotn;i++) { if(!visited[i]) { dfs(i,time); } } } int main() { freopen("in.txt","r",stdin); Gadow(); return 0; }