您的位置:首页 > 其它

求强连通分量——Tarjan、Kosaraju算法

2014-04-11 00:01 253 查看

1、强连通分量

    有向图强连通分量在有向图G中,如果两个顶点vi,vj间有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。

2、求强连通分量

    目前有三种时间效率为O(N)的求强连通分量的算法,分别为Tarjan、Kosaraju和Gabow算法,其中Gabow算法很少被使用,这里只介绍前两种算法。

3、Tarjan算法

    求有向图的强连通分量,按照朴素的思路来做,可以分别求正向连通分量和反向连通分量,然后求交集。显然时间效率达到O(N^2)。Tarjan算法有更好的办法。

【算法思路】Tarjan算法是基于对图深度优先搜索(dfs)的算法。遍历搜索树前先初始化low数组,用来记录回路入口,即强连通分量的代表结点的时间戳。在用dfs遍历搜索树的时候,搜索树中每个点分配一个时间戳,遍历树中遍历到的点填写时间戳和low数组值(都填当前时间)并入栈,如果遍历到已经入栈的点,即出现回路,也就是找到了一个强连通分量,回溯时把回路中各点的low值置为回路入口的时间戳值。如果有新回路包含了旧回路,则新回路中各点的low值将被置为更小的时间戳值。完成整个搜索树的遍历后,搜索树中各强连通分量已合并完毕,并以入口点的时间戳值进行了标记。

#include<vector>
using namespace std;

const int N = 1000;

vector<int> g
, s;              //邻接表和遍历树用的栈
int n, low
, dfn
, t;         //点数,low值数组,时间戳数组和当前时间
int sccID
, sccNum;             //点的scc归属数组和scc总数
bool inStack
;                  //标记点是否在栈内

void dfs(int v)
{
low[v] = dfn[v] = ++t;        //填写当前点的low值和时间戳值
s.push_back(v);               //当前点入栈
inStack[v] = true;            //入栈标记
for( int i = 0 ; i < g[v].size() ; i++ )
{
int u = g[v][i];
if( !dfn[u] )             //如果点还没有遍历过
{
dfs(u);
if( low[u] < low[v] )
low[v] = low[u];  //回溯时更新low值
}
else if( inStack[u] && dfn[u] < low[v] )
low[v] = dfn[u];      //如果找到了回路,更新low值
}
if( low[v] != dfn[v] )        //如果遍当前点不是强连通分量的入口
return;
int top;
do                            //强连通分量出栈
{
top = s.back();
inStack[top] = false;
sccID[top] = sccNum;      //填写点的强连通分量归属
s.pop_back();
}while( top != v );
sccNum++;                     //更新强连通分量数量
}

void Tarjan()
{
s.clear();                    //初始化栈
for( int i = 0 ; i < n ; i++ )
dfn[i] = 0;               //初始化时间戳
t = 0;                        //初始化当前时间
sccNum = 0;                   //初始化scc数
for( int i = 0 ; i < n ; i++ )
if(!dfn[i])
dfs(i);                //遍历所有点
}


4、Kosaraju算法

【算法原理】一个有向图的强连通分量与其逆图是一样的(即假如任意顶点s与t属于原图中的一个强连通分量,那么在逆图中这两个顶点必定也属于同一个强连通分量,这个事实由强连通性的定义可证)。Kosaraju的结论是,在第二次dfs中,同一棵搜索树上的结点属于一个强连通分量。

【算法步骤】

①在该图的逆图(所有边做反向)上运行dfs,将顶点按照后序编号(后序遍历:对一个结点来说先给其所有子结点依次编号,然后给其本身编号)的顺序放入一个数组中(这个过程作用在DAG上得到的就是一个拓扑排序);

②在原图上,按第一步得出的后序编号的逆序进行dfs。也就是说,在第二次dfs时,每次都挑选当前未访问的结点中具有最大后序编号的顶点作为dfs树的树根。实际效果就是在原图上按照逆图中由根结点到子结点的遍历顺序依次建立遍历树进行遍历。

(实际上原图和逆图颠倒一下结果并没有什么不同)

【算法评价】Kosaraju算法虽然是线性的,但是需要两次dfs,跟另外两个著名的求解强连通分量的算法相比,这是一个劣势。但是Kosaraju算法有个神奇之处在于:计算之后的强连通分量编号的顺序( sccID[ topo[ i ] ] ),刚好是该有向图K(D)(kernelDAG,核心DAG:将原图中每个强连通分量缩成一个点形成的有向无环图)的一个拓扑排序!因此Kosaraju算法同时提供了一个计算有向图K(D)拓扑排序的线性算法。这个结果在一些应用中非常重要。#include<cstring>
#include<vector>
using namespace std;

const int N = 1000;

vector<int> g
, gn
; //建立原图和逆图
int n, t, topo
; //点数,当前遍历时间和拓扑排序序列
int sccNum, sccID
; //scc数和点的scc归属
bool vis
; //标记访问过的点

void dfs( int v ) //目的是遍历所有点,建立后序遍历的拓扑排序序列
{
if( vis[v] )
return;
vis[v] = true;
for( int i = 0 ; i < g[v].size() ; i++ )
if( !vis[g[v][i]] )
dfs(g[v][i]);
topo[t++] = v; //后序遍历,回溯时根结点后进入拓扑序列
}
void ndfs( int v ) //反向遍历拓扑序列
{
if( sccID[v] )
return;
sccID[v] = sccNum; //同一棵子树上的点属于同一个scc
for( i = 0 ; i < gn[v].size() ; i++ )
if( !id[gn[v][i]] )
ndfs(gn[v][i]);
}
void Kosaraju()
{
t = 0; //初始化当前时间
memset(vis,false,sizeof(vis)); //初始化访问标记
for( int i = 0 ; i < n ; i++ ) //遍历原图建立拓扑序列
if( !vis[i] )
dfs(i);
sccNum = 0; //初始化scc数
for( int i = 0 ; i < n ; i++ )
sccID[i] = 0; //初始化点的scc归属
for( int i = t-1 ; i >= 0 ; i-- )//逆序遍历拓扑序列
if( !sccID[topo[i]] )
{
sccNum++; //强连通分量编号不可以使用0,先自加
ndfs(topo[i]);
}
}这两个算法我就不特别写邻接矩阵版本的模板了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息