求解有向图的强连通分量的SCC问题---POJ 2186 Popular Cows

来源:互联网 发布:夏普2048n网络扫描 编辑:程序博客网 时间:2024/05/16 06:25

【SCC问题】

在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected),如果有向图G的每两个顶点都强连通,称G是一个强连通图.通俗的说法是:从图G内任意一个点出发,存在通向图G内任意一点的的一条路径.

非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components,SCC).

求图强连通分量的意义是:由于强连通分量内部的节点性质相同,可以将一个强连通分量内的节点缩成一个点(重要思想),即消除了环,这样,原图就变成了一个有向无环图(directed acyclic graph,DAG).显然对于一个无向图,求强连通分量没有什么意义,连通即为强连通.

【求解算法】

求解有向图强连通分量主要有3 个算法:Tarjan 算法、Kosaraju 算法和Gabow 算法,本文主要介绍Tarjan算法和Kosaraju算法的思想和实现过程。

1.Tarjan算法

0)预备知识

在介绍算法之前,先介绍几个基本概念。


DFS搜索树:用DFS对图进行遍历时,按照遍历次序的不同,我们可以得到一棵DFS搜索树,如图(b)所示。

树边:(又称为父子边),在搜索树中的实线所示,可理解为在DFS过程中访问未访问节点时所经过的边。

回边:(又称为返祖边后向边),在搜索树中的虚线所示,可理解为在DFS过程中遇到已访问节点时所经过的边。

我们用dfn[u]记录节点u在DFS过程中被遍历到的次序号,low[u]记录节点u或u的子树通过非父子边追溯到最早的祖先节点(即DFS次序号最小),那么low[u]的计算过程如下:



1)基本原理

Tarjan 算法是基于DFS 算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。当dfn(u)=low(u)时,以u 为根的搜索子树上所有节点是一个强连通分量。

算法伪代码如下:

algorithm tarjan is  input: 图 G = (V, E)  output: 以所在的强连通分量划分的顶点集  index := 0  S := empty    // 置栈为空  for each v in V do    if (v.index is undefined)      strongconnect(v)    end if  function strongconnect(v)    // 将未使用的最小index值作为结点v的index    v.index := index    v.lowlink := index    index := index + 1    S.push(v)    // 考虑v的后继结点    for each (v, w) in E do      if (w.index is undefined) then        // 后继结点w未访问,递归调用        strongconnect(w)        v.lowlink := min(v.lowlink, w.lowlink)      else if (w is in S) then        // w已在栈S中,亦即在当前强连通分量中        v.lowlink := min(v.lowlink, w.index)      end if    // 若v是根则出栈,并求得一个强连通分量    if (v.lowlink = v.index) then      start a new strongly connected component      repeat        w := S.pop()        add w to current strongly connected component      until (w = v)      output the current strongly connected component    end if  end function

2)算法演示

Byvoid在这里做了一个详细的介绍和演示。我搬运一下:

1. 从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。


2. 返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。


3. 返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。

4. 继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。


至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。

3)复杂度分析

运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(V+E)

当图是使用邻接矩阵形式组建的,算法的时间复杂度为O(V^2)

4)备注

1. 判断结点是否在栈中应在常数时间内完成,例如可以对每个结点保存一个是否在栈中的标记。

2. 同一个强连通分量内的结点是无序的,但此算法具有如下性质:每个强连通分量都是在它的所有后继强连通分量被求出之后求得的。因此,如果将同一强连通分量收缩为一个结点而构成一个有向无环图,这些强连通分量被求出的顺序是这一新图的拓扑序的逆

2.Kosaraju 算法

1)基本原理

Kosaraju算法利用了有向图的这样一个性质,一个图和他的逆图transpose graph(边全部反向)具有相同的强连通分量。

Kosaraju算法的原理为:如果有向图G 的一个子图G'是强连通子图,那么各边反向后没有任何影响,G'内各顶点间仍然连通,G'仍然是强连通子图。但如果子图G'是单向连通的,那么各边反向后可能某些顶点间就不连通了,因此,各边的反向处理是对非强连通块的过滤。

算法伪代码:

Kosaraju's algorithm is simple and works as follows:

  • Let G be a directed graph and S be an empty stack.
  • While S does not contain all vertices:
    • Choose an arbitrary vertex v not in S. Perform a depth-first search starting at v.Each time that depth-first search finishes expanding a vertex u, pushu onto S.
  • Reverse the directions of all arcs to obtain the transpose graph.
  • While S is nonempty:
    • Pop the top vertex v from S. Perform a depth-first search starting at v. The set of visited vertices will give the strongly connected component containing v; record this and remove all these vertices from the graph G and the stack S. Equivalently,breadth-first search (BFS) can be used instead of depth-first search.

2)复杂度分析

当图是使用邻接表形式组建的,Kosaraju算法需要对整张图进行了两次的完整的访问,每次访问与顶点数V和边数 E之和 V+E成正比,所以可以在线性时间O(V+E)内访问完成。该算法在实际操作中要比Tarjan算法要慢,Tarjan算法只需要对图进行一次完整的访问。

当图是使用邻接矩阵形式组建的,算法的时间复杂度为O(V^2)

3)拓扑排序

拓扑排序有两种方法,具体方法可以看这篇博客。其中一种就是基于DFS的拓扑排序。

kosaraju算法在对原图进行DFS的时候在递归返回后,Each time that depth-first search finishes expanding a vertex u, pushu onto S.实际上和使用基于DFS的拓扑排序中添加顶点到最终结果栈中的过程几乎一致,只不过,只不过这里的图不一定是DAG,而拓扑排序中的图一定是DAG。

对非有向无环图来说,是不能进行拓扑排序的,所以实际上的S栈中的的序列实际上是“伪拓扑排序”,因为最终的结果不一定满足拓扑排序中严格的偏序定义,这是由于回边的存在。

而而这些回向边,就是构成强连通分量的关键。为了突出这些回向边,Kosaraju算法将图进行转置后,原来的回向边就都变成正向边了,对转置后的图按照上面”伪拓扑排序“中顶点出现的顺序调用DFS,而每次调用DFS形成的一颗搜索树,就构成了原图中的一个强连通分量。

4)备注

该算法和Tarjan算法具有相似(相反?)的性质:每个强连通分量都是在它的所有后继强连通分量被求出之前求得的。这是因为在对原图进行DFS时得到了“伪拓扑序的逆”,再对逆图进行DFS的时候是按照这个“伪拓扑序的逆的逆进行的,所以连通分量求出顺序和原图缩点后得到新图的拓扑序一致。因此,如果将同一强连通分量收缩为一个结点而构成一个有向无环图,这些强连通分量被求出的顺序是这一新图的拓扑序

【算法应用:POJ2186】

题意:每头奶牛都梦想着成为牧群中最受奶牛仰慕的奶牛。在牧群中,有N 头奶牛,1≤N≤10,000,给定M 对(1≤M≤50,000)有序对(A, B),表示A 仰慕B。由于仰慕关系具有传递性,也就是说,如果A 仰慕B,B 仰慕C,则A 也仰慕C,即使在给定的M 对关系中并没有(A, C)。你的任务是计算牧群中受每头奶牛仰慕的奶牛数量。

题解:

由于仰慕的传递性,一强连通分量内的牛都互相仰慕,并且如果一个连通分量A内的一头牛仰慕另一个连通分量B内的另一头牛,则A连通分量内的所有牛都仰慕B连通分量的所有牛。所以讲图中的连通分量缩为一个点,构造新图,则这个图为DAG。最后作一次扫描,统计出度为0 的顶点个数,如果正好为1,则说明该顶点(是一个新构造的顶点,即对应一个强连通分量)能被其他所有顶点走到,即该强连通分量为所求答案,输出它的顶点个数即可。

代码1:(Tarjan算法)

#include<iostream>#include<cstdio>#include<cstring>#include<algorithm>#include<queue>#include<stack>using namespace std;const int MAX=10000+10;vector<int> g[MAX];//——邻接表存图stack<int> st;//—栈int dfn[MAX],low[MAX],instack[MAX];//dfn——dfs访问次序,low——通过其子树能访问到的最小的dfn,instack——是否在栈中int sccno[MAX],sccsize[MAX];//sccno——每个点强连通分量的标号,sccsize——每个强连通分量内点的个数int out[MAX];//——强连通分量的出度int dfs_cnt,scc_cnt;//dfs_cnt——dfs访问序号,scc_cnt——强连通分量个数int n,m;//点数,边数void dfs(int u){dfn[u]=low[u]=++dfs_cnt;instack[u]=1;st.push(u);for(int i=0;i<g[u].size();i++){int v=g[u][i];if(!dfn[v]){dfs(v);low[u]=min(low[u],low[v]);}else if(instack[v])low[u]=min(low[u],dfn[v]);}if(dfn[u]==low[u]){scc_cnt++;int v;do{v=st.top();st.pop();instack[v]=0;sccno[v]=scc_cnt;sccsize[scc_cnt]++;}while(u!=v);}}void tarjan(){dfs_cnt=scc_cnt=0;memset(dfn,0,sizeof(dfn));memset(instack,0,sizeof(instack));memset(sccno,0,sizeof(sccno));memset(sccsize,0,sizeof(sccsize));while(!st.empty()) st.pop();for(int i=1;i<=n;i++){if(!dfn[i]) dfs(i);}}int main(){//freopen("in.txt","r",stdin);//freopen("out.txt","w",stdout);while(scanf("%d%d",&n,&m)!=EOF){for(int i=1;i<=n;i++)g[i].clear();for(int i=0;i<m;i++){int u,v;scanf("%d%d",&u,&v);g[u].push_back(v);}tarjan();memset(out,0,sizeof(out));for(int u=1;u<=n;u++){for(int j=0;j<g[u].size();j++){int v=g[u][j];if(sccno[u]!=sccno[v]){out[sccno[u]]++;}}}int ans,cou=0;for(int i=1;i<=scc_cnt;i++){if(out[i]==0){ans=sccsize[i];cou++;}}printf("%d\n",cou==1?ans:0);}return 0;}

代码2:(Kosaraju算法)(参考:http://blog.csdn.net/whai362/article/details/46964883)

这里进行了一个改进,根据Kosaraju算法的性质(连通分量求出顺序为缩点后新图的拓扑序),Kosaraju算法完成后,拓扑排序其实也完成了,判断是否与所有连通分量都和拓扑序终点相连即可,如果是则结果为其顶点数,否则为0。

#include<iostream>#include<cstdio>#include<cstring>#include<algorithm>#include<vector>using namespace std;const int MAX=10000+10;vector<int> G[MAX];vector<int> rG[MAX];vector<int> vs;int vis[MAX];int sccno[MAX];int scc_cnt;int n,m;void dfs(int u){vis[u]=1;for(int i=0;i<G[u].size();i++){int v=G[u][i];if(!vis[v]) dfs(v);}vs.push_back(u);}void rdfs(int u){sccno[u]=scc_cnt;vis[u]=1;for(int i=0;i<rG[u].size();i++){int v=rG[u][i];if(!vis[v]) rdfs(v);}}void kosaraju(){vs.clear();scc_cnt=0;memset(sccno,0,sizeof(sccno));memset(vis,0,sizeof(vis));for(int i=1;i<=n;i++){if(!vis[i]) dfs(i);}memset(vis,0,sizeof(vis));for(int i=vs.size()-1;i>=0;i--){int v=vs[i];if(!vis[v]) {scc_cnt++;rdfs(v);}}}int main(){//freopen("in.txt","r",stdin);//freopen("out.txt","w",stdout);while(scanf("%d%d",&n,&m)!=EOF){for(int i=1;i<=n;i++){G[i].clear();rG[i].clear();}for(int i=0;i<m;i++){int a,b;scanf("%d%d",&a,&b);G[a].push_back(b);rG[b].push_back(a);}kosaraju();int ans=0,u;for(int i=1;i<=n;i++){if(sccno[i]==scc_cnt) {u=i;ans++;}}memset(vis,0,sizeof(vis));rdfs(u);for(int i=1;i<=n;i++){if(!vis[i]){ans=0;break;}}printf("%d\n",ans);}return 0;}

参考资料:

1.Tarjan算法 - 维基百科,自由的百科全书

2.【图论】求无向连通图的割点

3.求解强连通分量算法之---Kosaraju算法

4.拓扑排序的原理及其实现

5.有向图强连通分量的Tarjan算法 - BYVoid

6.强连通分量分解 Kosaraju算法 (poj 2186 Popular Cows)

7.《图论算法理论、实现及应用》

0 0
原创粉丝点击