算法导论 第22章 图的基本算法(四) 强连通分支

来源:互联网 发布:淘宝装修代码下载 编辑:程序博客网 时间:2024/05/17 17:57

强连通分支

   计算有向图的强连通分支是深度优先搜索经典应用,很多图相关的算法都是将图分解为各个强连通分支之后,然后在各个强连通分支上运行,最后根据各个强连通分支之间的关系将所有的解组合起来。

   对于有向图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,过程结束后原图顶点状态改变,得到访问结束时间;

  2、reverse对原图求边的转置,存储于reG变量中的边集合E中,稍后获得原图顶点的转态信息,同时更改颜色为白色,因为接下来还要进行一次DFS;

  3、对reG中的顶点按照compare的规则排序,即按照访问时间的结束时刻,从大到小排序;

  4、对reG再一次DFS求得强连通分支,采用函数SCC_DFS,该函数通过递归的DFS修改而来,代码见下:

void AGraph::SCC_DFS(vector<SCCvertex> &branch){size_t branchnum = 0;for (size_t i = 1; i != E.size(); ++i)if (V[i].c == WHITE){//新强连通分支出现SCCvertex tmp_scc;tmp_scc.sccID = ++branchnum;//分配强连通分支编号tmp_scc.sccSet.push_back(V[i].id);//该强连通分支包含的顶点SCC_DFS_aux(V[i].id,tmp_scc);//其他顶点branch.push_back(tmp_scc);}}

   在for循环中,如果当前顶点为白色,说明没有被访问过,新的强连通分支出现,设置强连通编号,收入该顶点,然后调用SCC_DFS_aux进一步深度优先搜索确定该强连通分支的其他顶点,最后将其存入branch中,注意:此处branch没有从索引1开始存储信息,因为我们并不知道会有多少个强连通分支,不过编号减1即为索引。

SCC_DFS_aux代码:

void AGraph::SCC_DFS_aux(size_t v,SCCvertex &tmp_scc){size_t index = vertexIndex(v);//取的节点v在V中的索引,以正确从E中获得其边信息,下同V[index].c = GRAY;edgeNode *curr = E[v];while (curr != nullptr){size_t i = vertexIndex(curr->adjvertex);if (V[i].c == WHITE){//若为白色节点,则为该强连通分支一员tmp_scc.sccSet.push_back(V[i].id);SCC_DFS_aux(V[i].id,tmp_scc);}curr = curr->nextEdge;}V[index].c = BLACK;}

vertexIndex函数时确定v号顶点在图的V集合中的索引的。若排序我们采用计数排序,则整个事件复杂度将会是O(V+E),采用内置sort事件为O(VlgV+E)。

最后采用一个循环输出branch中的强连通信息即可。对下图从顶点a开始顺时针编号后,运行算法得到结果如下:


习题22.5-5 求取分支图

    思路:得到图的各强连通分支集合branch后,遍历原图的邻接表E,对于任意边(u,v),若两者所属的强连通分支不同,分支编号分别为x和y,那么就将在分支图中加入边(x,y),若相同则继续遍历,下面给出代码,细节问题将在注释讨论。


计算有向图的分支图代码

void AGraph::branchGraph(vector<SCCvertex> &branch,AGraph &branchG){//求该图的分支图,存储于branchG中;branch存储强连通分支信息,时间O(V+E)stronglyConnectedCompenents(branch);//获取图的强连通分支信息,时间O(V+E),用计数排序。branchG.editGraph(branch.size());//根据分支数更改图vector<size_t> vertex_branch_id(nodenum + 1);//顶点的SCC编号,索引即为顶点编号for (size_t i = 0; i != branch.size(); ++i)//将每个顶点的分支id记下来,便于后续操作,时间O(V)for (size_t j = 0; j != branch[i].sccSet.size(); ++j)vertex_branch_id[branch[i].sccSet[j]] = branch[i].sccID;for (size_t i = 1; i != E.size(); ++i){//遍历图的边表E,时间O(V+E)edgeNode *curr = E[i];while (curr != nullptr){//(1)如果相连两顶点(u和v)的分支id(x和y)不同,则加入该分支边(x,y),不用担心分支图中已经存在//边(y,x),因为如果存在此边,则说明在分支y中有顶点能够到达分支x,现在又有从分支x到分支y//的边(u,v),那么这两个分支是能够互达的,应该是同一个分支,矛盾。//(2)若分支图中已经存在边(x,y),则不会添加,因为addEdge会先查找是否有此边if (vertex_branch_id[i] != vertex_branch_id[curr->adjvertex])branchG.addEdge(vertex_branch_id[i], vertex_branch_id[curr->adjvertex]);curr = curr->nextEdge;}}}

代码中的vertex_branch_id存储了各个顶点的所属的分支编号,便于while中比较。最后的分支图存储在branchG中。时间O(VlgV+E),题目要求O(V+E),只要将计算强连通分支中的排序改为计数排序就可以了。上图运行结果


习题22.5-6 计算有向连通图的简化图

    思路:如何是边最少,且满足要求?显然,只加入必要的边,其他边均舍弃。根据强连通分支信息,对每个分支所包含的顶点循环加入添加边,然后根据分支图中各强连通分支的邻接表,遍历每条边,如边(x,y),分别取分支x,y中的任意顶点u,v,添加边(u,v)即可,细节将在代码注释中讨论。

void AGraph::simplifyGraph(AGraph &simpleG){//简化图,存储于simpleG中,使其强连通分支和分支图与原图一样,且边尽量小。 //思路如下:vector<SCCvertex> branch;AGraph branchG;branchGraph(branch, branchG);//1、运用分支图函数求出原图的强连通分支信息和分支图信息for (size_t i = 0; i != branch.size(); ++i){//2、对同一个分支里的顶点循环插入边,如某分支中有顶点v1,v2,v3...vk,则插入边(v1,v2),(v2,v3)...(vk,v1)//若分支只有一个顶点,则不作处理size_t i_branch_size = branch[i].sccSet.size();for (size_t j = 0; i_branch_size > 1 && j != i_branch_size; ++j)simpleG.addEdge(branch[i].sccSet[j % i_branch_size], branch[i].sccSet[(j + 1) % i_branch_size]);}for (size_t i = 1; i != branchG.E.size(); ++i){//3、再根据分支图中各分支之间的关系插入边,如分支编号为x和y的两分支存在边(x,y),则任取x和y中一个顶点//假设为u,v,插入边(u,v)edgeNode *curr = branchG.E[i];while (curr != nullptr){//branch是从0开始存储分支的,而分支编号从1开始,并且分支j存储在索引j - 1中,参考SCC_DFS //的“注意”,因而分支编号i减1即为索引。运用了一点小把戏,最好的做法应该是查找出该分支的索引simpleG.addEdge(branch[i - 1].sccSet[0], branch[curr->adjvertex - 1].sccSet[0]);curr = curr->nextEdge;}}}


时间复杂度为O(VlgV+E),若采用计数排序,则时间为O(V+E)。上图运行结果


习题22.5-7 判断半连通图

    1、用习题22.5-5中的算法计算得分支图;O(V+E)

    2、对该分支图进行拓扑排序,得到序列,<v0,v1,v2...vk>;O(V+E)

    3、若在分支图中存在边(v0,v1),(v1,v2)...(vk-1,vk),则为半连通,否则为非。O(V+E)

时间复杂度O(V+E),前提为采用计数排序。


本节所有用到的代码摘录如下

#include<iostream>#include<fstream>#include<vector>#include<stack>#include<algorithm>#define NOPARENT 0using namespace std;enum color{ WHITE, GRAY, BLACK };struct edgeNode{//边节点size_t adjvertex;//该边的关联的顶点int weight;//边权重edgeNode *nextEdge;//下一条边edgeNode(size_t adj, int w) :adjvertex(adj), weight(w), nextEdge(nullptr){}};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){}};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;}};class AGraph{//图private://边信息容器和顶点信息容器均从索引1开始存储信息,同时所有的图顶点也从1开始编号,因此容器中索引i对应地//就是顶点i及其边信息,顶点容器中顶点编号和索引的对应关系在需要移动顶点的操作下会错位,边容器一般不变vector<edgeNode*> E;vector<vertex> V;size_t nodenum;//顶点数void printEdge();void printVertex();void SCC_DFS(vector<SCCvertex>&);//求强连通分支void DFS_aux_Not_recursive(size_t, size_t &);//非递归的DFS辅助函数void SCC_DFS_aux(size_t,SCCvertex&);//求强连通分支辅助函数,修改递归的DFS而得size_t vertexIndex(size_t id){//查找id号顶点的在顶点容器中的索引size_t id_index;for (size_t i = 1; i != V.size(); ++i)if (V[i].id == id) id_index = i;return id_index;}void init(size_t n){E.resize(n);V.resize(n);for (size_t i = 1; i != n; ++i)V[i].id = i;}void editGraph(size_t n) { init(n + 1); }//修改图大小,并初始化public:AGraph(size_t n = 0) :nodenum(n){ init(n + 1); }void initGraph();//初始化有向图edgeNode* search(size_t, size_t);//查找边void addEdge(size_t, size_t, int);//有向图中添加边void reverse(AGraph *);//求图的转置void DFS();void stronglyConnectedCompenents(vector<SCCvertex>&);//求强连通分量void branchGraph(vector<SCCvertex>&,AGraph&);//求分支图void simplifyGraph(AGraph&);//简化图void print();~AGraph();};void AGraph::initGraph(){size_t start, end;ifstream infile("F:\\scc.txt");while (infile >> start >> end)addEdge(start, end, 1);}edgeNode* AGraph::search(size_t start, size_t end){edgeNode *curr = E[start];while (curr != nullptr && curr->adjvertex != end)curr = curr->nextEdge;return curr;}void AGraph::addEdge(size_t start, size_t end, int weight = 1){edgeNode *curr = search(start, end);//插入边之前查找是否已存在if (curr == nullptr){edgeNode *p = new edgeNode(end, weight);p->nextEdge = E[start];E[start] = p;}}void AGraph::reverse(AGraph *regraph){for (size_t i = 1; i != E.size(); ++i){edgeNode *curr = E[i];while (curr != nullptr){regraph->addEdge(curr->adjvertex, i);curr = curr->nextEdge;}}}inline void AGraph::printEdge(){for (size_t i = 1; i != E.size(); ++i){edgeNode *curr = E[i];cout << i;if (curr == nullptr) cout << " --> null";elsewhile (curr != nullptr){cout << " --> " << curr->adjvertex;curr = curr->nextEdge;}cout << endl;}}inline void AGraph::printVertex(){cout << "vertex ID" << endl;for (size_t i = 1; i != V.size(); ++i)printf("%-4d ", V[i].id);cout << endl << "vertex color" << endl;for (size_t i = 1; i != V.size(); ++i)printf("%-4d ", V[i].c);cout << endl << "vertex parent" << endl;for (size_t i = 1; i != V.size(); ++i)printf("%-4d ", V[i].p);cout << endl << "vertex start time" << endl;for (size_t i = 1; i != V.size(); ++i)printf("%-4d ", V[i].d);cout << endl << "vertex finish time" << endl;for (size_t i = 1; i != V.size(); ++i)printf("%-4d ", V[i].f);cout << endl;}inline void AGraph::print(){printEdge();//printVertex();}void AGraph::SCC_DFS(vector<SCCvertex> &branch){size_t branchnum = 0;for (size_t i = 1; i != E.size(); ++i)if (V[i].c == WHITE){//新强连通分支出现SCCvertex tmp_scc;tmp_scc.sccID = ++branchnum;//分配强连通分支编号tmp_scc.sccSet.push_back(V[i].id);//该强连通分支包含的顶点SCC_DFS_aux(V[i].id,tmp_scc);//其他顶点branch.push_back(tmp_scc);}}void AGraph::SCC_DFS_aux(size_t v,SCCvertex &tmp_scc){size_t index = vertexIndex(v);//取的节点v在V中的索引,以正确从E中获得其边信息,下同V[index].c = GRAY;edgeNode *curr = E[v];while (curr != nullptr){size_t i = vertexIndex(curr->adjvertex);if (V[i].c == WHITE){//若为白色节点,则为该强连通分支一员tmp_scc.sccSet.push_back(V[i].id);SCC_DFS_aux(V[i].id,tmp_scc);}curr = curr->nextEdge;}V[index].c = BLACK;}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;}struct compare{//局部函数对象类,定义sort的比较规则bool operator()(const vertex &lhs, const vertex &rhs)const{return lhs.f > rhs.f;}};sort(++reG.V.begin(), reG.V.end(), compare());//按访问结束时间降序排序,时间O(VlgV),用计数排序可达到O(V)reG.SCC_DFS(branch);//求强连通分支,由递归的DFS修改而得,时间O(V+E)}void AGraph::branchGraph(vector<SCCvertex> &branch,AGraph &branchG){//求该图的分支图,存储于branchG中;branch存储强连通分支信息,时间O(V+E)stronglyConnectedCompenents(branch);//获取图的强连通分支信息,时间O(V+E),用计数排序。branchG.editGraph(branch.size());//根据分支数更改图vector<size_t> vertex_branch_id(nodenum + 1);//顶点的SCC编号,索引即为顶点编号for (size_t i = 0; i != branch.size(); ++i)//将每个顶点的分支id记下来,便于后续操作,时间O(V)for (size_t j = 0; j != branch[i].sccSet.size(); ++j)vertex_branch_id[branch[i].sccSet[j]] = branch[i].sccID;for (size_t i = 1; i != E.size(); ++i){//遍历图的边表E,时间O(V+E)edgeNode *curr = E[i];while (curr != nullptr){//(1)如果相连两顶点(u和v)的分支id(x和y)不同,则加入该分支边(x,y),不用担心分支图中已经存在//边(y,x),因为如果存在此边,则说明在分支y中有顶点能够到达分支x,现在又有从分支x到分支y//的边(u,v),那么这两个分支是能够互达的,应该是同一个分支,矛盾。//(2)若分支图中已经存在边(x,y),则不会添加,因为addEdge会先查找是否有此边if (vertex_branch_id[i] != vertex_branch_id[curr->adjvertex])branchG.addEdge(vertex_branch_id[i], vertex_branch_id[curr->adjvertex]);curr = curr->nextEdge;}}}void AGraph::simplifyGraph(AGraph &simpleG){//简化图,存储于simpleG中,使其强连通分支和分支图与原图一样,且边尽量小。 //思路如下:vector<SCCvertex> branch;AGraph branchG;branchGraph(branch, branchG);//1、运用分支图函数求出原图的强连通分支信息和分支图信息for (size_t i = 0; i != branch.size(); ++i){//2、对同一个分支里的顶点循环插入边,如某分支中有顶点v1,v2,v3...vk,则插入边(v1,v2),(v2,v3)...(vk,v1)//若分支只有一个顶点,则不作处理size_t i_branch_size = branch[i].sccSet.size();for (size_t j = 0; i_branch_size > 1 && j != i_branch_size; ++j)simpleG.addEdge(branch[i].sccSet[j % i_branch_size], branch[i].sccSet[(j + 1) % i_branch_size]);}for (size_t i = 1; i != branchG.E.size(); ++i){//3、再根据分支图中各分支之间的关系插入边,如分支编号为x和y的两分支存在边(x,y),则任取x和y中一个顶点//假设为u,v,插入边(u,v)edgeNode *curr = branchG.E[i];while (curr != nullptr){//branch是从0开始存储分支的,而分支编号从1开始,并且分支j存储在索引j - 1中, //因而分支编号i减1即为该分支索引。运用了一点小把戏,最好的做法应该是查找出该分支的索引simpleG.addEdge(branch[i - 1].sccSet[0], branch[curr->adjvertex - 1].sccSet[0]);curr = curr->nextEdge;}}}void AGraph::DFS_aux_Not_recursive(size_t u, size_t &time){//非递归的DFSstack<size_t> S;vector<edgeNode*> access_edge(E);//记下每个顶点下一条将被访问的边V[u].c = GRAY;V[u].d = ++time;S.push(u);while (!S.empty()){//只要栈不空,不断访问size_t i = S.top();edgeNode *curr = access_edge[i];//得到顶点i当前将要被访问的边while (curr != nullptr){//不断循环,直到访问到一个白节点,或者顶点i的所有邻接点已被访问if (V[curr->adjvertex].c == WHITE){//与i相邻的是白节点,即未被访问过V[curr->adjvertex].c = GRAY;V[curr->adjvertex].d = ++time;V[curr->adjvertex].p = i;S.push(curr->adjvertex);//访问后入栈access_edge[i] = curr->nextEdge;//记下顶点i下一条将要被访问的边break;}else curr = curr->nextEdge;}if (curr == nullptr){//顶点i的所有邻接点已被访问,则出栈V[i].c = BLACK;V[i].f = ++time;S.pop();}}}void AGraph::DFS(){size_t time = 0;for (size_t i = 1; i != E.size(); ++i)if (V[i].c == WHITE)DFS_aux_Not_recursive(i, time);}AGraph::~AGraph(){for (size_t i = 1; i != E.size(); ++i){edgeNode *curr = E[i], *pre;while (curr != nullptr){pre = curr;curr = curr->nextEdge;delete pre;}}}const int nodenum = 8;int main(){AGraph graph(nodenum), /*branchG*/simpleG(nodenum);//vector<SCCvertex> v;graph.initGraph();graph.print();cout << endl;/*graph.branchGraph(v,branchG);for (size_t i = 0; i != v.size(); ++i)v[i].print();cout << endl;branchG.print();*/graph.simplifyGraph(simpleG);simpleG.print();cout << endl;getchar();return 0;}







0 0
原创粉丝点击