强连通分支
计算有向图的强连通分支是深度优先搜索经典应用,很多图相关的算法都是将图分解为各个强连通分支之后,然后在各个强连通分支上运行,最后根据各个强连通分支之间的关系将所有的解组合起来。
对于有向图G=(V,E)任意两个顶点u,v,如果u能够经过一条路径到达v,v也能通过一条路径到达u,即两者互相可达,它们将属于一个强连通分支,强连通用分支就是这样一个最大的互相可达的顶点集合。
在本篇博客所讲述的求解强连通分支算法中,会用到图G的转置G^T,参考图的基本算法(一) 习题22.1-3。算法的流程非常简单,如下:
1、进行一次DFS得到各顶点的访问结束时刻;
2、求取图G的转置G^T;
3、根据访问结束时刻从大到小的顺序对G^T再进行一次DFS,在此过程中,能够被DFS到顶点必在同一个强连通分支中,据此得到各个强连通分支;
4、输出各个强连通分支。
需要使用的数据结构及些许约定
1、边节点结构
struct edgeNode
{//边节点
size_t adjvertex;//该边的关联的顶点
int weight;//边权重
edgeNode *nextEdge;//下一条边
edgeNode(size_t adj, int w) :adjvertex(adj), weight(w), nextEdge(nullptr){}
};
2、顶点结构
struct vertex
{//顶点
size_t id, c;//编号,颜色
size_t d, f;//访问开始和结束时间
size_t p;//父节点编号
vertex(size_t i = 0) :id(i), c(WHITE), p(NOPARENT), d(0), f(0){}
};
3、强连通分支节点结构
struct SCCvertex
{//强连通分支顶点
size_t sccID;//强连通分支编号
vector<size_t> sccSet;//该强连通分支包含的顶点
void print()
{
cout << "SCC " << sccID << " includes vertex : ";
for (size_t i = 0; i != sccSet.size(); ++i)
cout << sccSet[i] << ' ';
cout << endl;
}
};
4、所有顶点,包括图顶点、强连通分支编号和习题的分支图顶点标号均从1开始递增;
5、图的基本数据成员
class AGraph
{//图
private:
//边信息容器和顶点信息容器均从索引1开始存储信息,同时所有的图顶点也从1开始编号,因此容器中索引i对应地
//就是顶点i及其边信息,顶点容器中顶点编号和索引的对应关系在需要移动顶点的操作下会错位,边容器一般不变
vector<edgeNode*> E;
vector<vertex> V;
size_t nodenum;//顶点数
};
正如注释所说,对顶点集合V的处理会涉及到排序,因而会改变索引和标号的对应关系,所以顶点有id域;顶点移动是在转置图中进行的,原图顶点依然是对应的。默认情况下,vector都是从索引1处开始存储数据,有个特别情况会注明。
下面根据计算强连通分支的代码来解决一些问题
void AGraph::stronglyConnectedCompenents(vector<SCCvertex> &branch)
{//branch存储强连通分支信息,包括各强连通分支id以及包含的节点id,若用计数排序时间复杂度为O(V+E)
DFS();//先对图进行一次DFS,算得每个节点访问完成时间,时间O(V+E)
AGraph reG(nodenum);
reverse(&reG);//对图求转置,存入reG.E,时间O(V+E)
for (size_t i = 1; i != reG.V.size(); ++i)
{//获得图顶点信息,存入reG.V,同时更改顶点颜色
reG.V[i] = V[i];
reG.V[i].c = WHITE;
}
sort(++reG.V.begin(), reG.V.end(), compare);//按访问结束时间降序排序,时间O(VlgV),用计数排序可达到O(V)
reG.SCC_DFS(branch);//求强连通分支,由递归的DFS修改而得,时间O(V+E)
}
1、DFS函数采取的辅助函数时非递归的DFS_aux_Not_recursive,过程结束后原图顶点状态改变,得到访问结束时