-
2017-12-08 21:00:00
Kosaraju算法是求解有向图强连通分量(strong
connected component)的三个著名算法之一,能在线性时间求解出一个图的强分量。用Sedgewick爷爷的话说,就是“现代算法设计的胜利”。 什么是强连通分量?在这之前先定义一个强连通性(strong
connectivity)的概念:有向图中,如果一个顶点s到t有一条路径,t到s也有一条路径,即s与t互相可达,那么我们说s与t是强连通的。那么在有向图中,由互相强连通的顶点构成的分量,称作强连通分量。 首先说一些离散数学相关的结论,由强连通性的概念可以发现,这是一个等价关系。
证明:
一,按照有向图的约定,每个顶点都有到达自身的路径,即自环,即任意顶点s到s可达,满足自反性;
二,如果s与t是强连通的,则s到t存在路径,t到s存在路径,显然t与s也是强连通的,满足对称性;
三,如果r与s强连通,s与t强连通,则r与s互相可达,s与t互相可达,显然r与t也互相可达,满足传递性。
因此,强连通关系可导出一个等价类,这就是强连通分量。进一步的利用这结论可以知道,两个强连通分量之间木有交集(这个结论很重要)。事实上,图论与离散数学中的关系有非常密切的……关系。
在编程求解强连通分量时,通常做法是对顶点进行编号,拥有相同编号的顶点属于同一个强连通分量。在求解完之后,通过对编号的比较可以迅速判断两个顶点是否是强连通的。
------------------------------分割线-----------------------------------
Kosaraju算法过程上并不复杂。要求解一个有向图的强连通分量,第一步:在该图的逆图上运行DFS,将顶点按照后序编号的顺序放入一个数组中(显然,这个过程作用在DAG上得到的就是一个拓扑排序);第二步:在原图上,按第一步得出的后序编号的逆序进行DFS。也就是说,在第二次DFS时,每次都挑选当前未访问的结点中具有最大后序编号的顶点作为DFS树的树根。
Kosaraju算法的显著特征是,第一,引用了有向图的逆图;第二,需要对图进行两次DFS(一次在逆图上,一次在原图上)。而且这个算法依赖于一个事实:一个有向图的强连通分量与其逆图是一样的(即假如顶点任意顶点s与t属于原图中的一个强连通分量,那么在逆图中这两个顶点必定也属于同一个强连通分量,这个事实由强连通性的定义可证)。由于算法的时间取决于两次DFS,因此时间复杂度,对于稀疏图是O(V+E),对于稠密图是O(V²),可见这是一个线性算法。Kosaraju的结论是,在第二次DFS中,同一棵搜索树上的结点属于一个强连通分量。
证明:假设顶点s与t属于第二次DFS森林(注意,第二次是在原图上搜索)的同一棵树,r是这棵树的根结点。那么有以下两个事实:一,原图中由r可达s,这蕴含在逆图中从s到r有一条路径;二,r在逆图中的后序编号大于s(r是树根,因此r的后序编号比树中所有的其他结点的都大)。现在要证明的是在逆图中从r到s也是可达的。
好,两个事实结合起来:一,假设逆图中r到s不可达,且s到r存在路径,那么这条路径将使s的后序编号比r大,与事实一矛盾,排除;二,假设逆图中r到s存在路径,正是这条r到s的路径使得r有更大的后序编号,则r与s是强连通的,假设成立(看上去比较勉强,个人认为这应该是一个空证明)。显然,两个事实导出一个结论:逆图中,r与s互相可达。同理,r与t也互相可达,根据传递性,第二次DFS森林中同一棵树中的所有顶点构成一个强连通分量。
另一方面,会不会一个强连通分量的所有顶点没有出现在第二次DFS森林的同一颗树中呢?答案是:不会。因为根据DFS的性质,如果r与s强连通,那么由r开始的DFS必定能搜到s。
证毕。
可见Kosaraju的方法能够找出有向图的强连通分量,那么为什么这个方法可行呢?或者如何实现呢?这正是Kosaraju算法最为精妙的地方,关键在于第二次DFS选取的顺序:在第一次DFS中,将顶点按照后序编号存放,第二次DFS就按照这个顺序的逆序进行搜索,这保证每次选取的根结点(刚才证明中的r结点)都具有未访问结点中最大的后序编号,则搜索中拓展的结点的后序编号都比根结点小,这样也就满足了事实二。
补充:Kosaraju算法虽然是线性的,但是需要两次DFS,跟另外两个著名的求解强分量的算法相比,这是一个劣势。但是Kosaraju算法有个神奇之处在于:计算之后的强分量编号的顺序,刚好是该有向图K(D)(kernel
DAG, 核心DAG)的一个拓扑排序!因此Kosaraju算法同时提供了一个计算有向图K(D)拓扑排序的线性算法。这个结果在一些应用中非常重要。 最后附上我的实现~就一目了然啦~
---------------------------分割线again--------------------------------
//
Kosaraju算法邻接矩阵实现 static
int cnt, cntR, pre[MAXV], postR[MAXV]; int
Kosaraju(Graph G) { int v; // 初始化全局变量 cnt = cntR = 0; for (v = 0; v < G->V; ++v) pre[v] = postR[v] = -1; // 第一次DFS,计算逆图的后序编号 for (v = 0; v < G->V; ++v) if (pre[v] == -1) dfsPostR(G, v); cnt = 0; for (v = 0; v < G->V; ++v) G->sc[v] = -1; // G->sv[v]表示顶点v的强连通分量编号 // 第二次DFS,强连通分量编号 for (v = G->V - 1; v >= 0; --v) { // 注意搜索的顶点顺序是逆图后序编号的逆序 if (G->sc[postR[v]] == -1) { dfsSC(G, postR[v]); ++cnt; // 对一棵树编号之后计数器值加1 } } return cnt; // 返回强连通分量的个数 }
void
dfsPostR(Graph G, int v) { // 对逆图后序编号 int t; pre[v] = cnt++; for (t = 0; t < G->V; ++t) if (G->adj[t][v] == 1) // 注意!!!邻接矩阵引用逆图,因此是G->adj[t][v] if (pre[t] == -1) dfsPostR(G, t); postR[cntR++] = v; // 后序编号,注意是计数器做数组下标 }
void
dfsSC(Graph G, int v) { int t; G->sc[v] = cnt; // 计数器作为编号 for (t = 0; t < G->V; ++t) if (G->adj[v][t] == 1) if (G->sc[t] == -1) dfsSC(G, t); }
int
GraphSC(Graph G, int s, int t) { // 比较顶点的强连通分量编号即可判断是否强连通 return G->sc[s] == G->sc[t]; }
更多相关内容 -
Kosaraju算法详解
2020-08-29 11:10:00主要为大家详细介绍了Kosaraju算法,Kosaraju算法可以计算出一个有向图的强连通分量,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 -
kosaraju:Kosaraju 算法是一种线性时间算法,用于查找有向图的强连通分量
2021-07-13 05:47:27kosaraju 算法Kosaraju 算法是一种线性时间算法,用于查找有向图的强连通分量算法Kosaraju 算法的工作原理如下: 设 G 为有向图,S 为空栈。 虽然 S 不包含所有顶点: 选择不在 S 中的任意顶点 ''v''。从 ''v'' 开始... -
强连通分量的Kosaraju算法实现
2014-04-14 20:41:242. 用Kosaraju算法实现了强连通分量的求解。其中data中包含的GoolNodes测试集为Google提供的网页之间的连接经转化而来,每一个结点均代表一个网页。 3. 缺点:为了使用以前的CGraph类,强行添加了结点文件,其中第一... -
kosaraju算法_算法_
2021-10-01 04:08:56利用拓扑排序实现kosaraju算法具体实现及源码 -
13.1 Sharir-Kosaraju算法
2022-04-06 09:26:47从概念定义、到算法详解、Python实现讲解Kosaraju算法强连接组件
Sharir-Kosaraju算法,有时候也简称Kosajaru算法,其目的是为了计算出图的强连通分量。强连通分量,英文为strongly connected component,简称SCC。
首先,我们要先了解什么是强连接分量。强连接分量,是图的一部分,在这一部分里,每个点都可以连接到其他点。如下图(图片来自斯坦福大学),存在五个强连接分量:
可以看出,强连接分量要么是一个环,要么是几个边相邻的环,要么是两个反向平行边,要么是单独的一个点。而上图覆盖了四种情况。
求出强连接分量后,可以做什么?首先是可以凝结Condensation。凝结是将强连接分量缩成一个点,这对于大型图简化来说是非常重要的。Kusajaru算法
该算法分为三步:
1. DFS算法计算完成时间。DFS算法可以计算出每个节点的发现时间与完成时间。
2. 计算逆图,所谓逆图,就是将边进行反向后产生的图。
3. 在逆图上继续DFS,因为前面正向了,如果反向还能访问,那无疑是一个强连接组件。DFS以最大访问时间的节点开始,如此DFS找出所有的强连接组件。
以上图为例子,我们先计算访问时间,下图中的括号部分表示的是发现时间与访问时间:
然后再产生逆图:
按结束时间从大到小遍历,产生强连通组件,先是E,发现时间戳为26:
然后是A,发现时间戳为22:
然后是C,发现时间戳为19:
然后是K,发现时间戳为12:
然后是L,发现时间戳为11:
Python实现
# _*_ coding:utf-8 _*_ class UnweightedGraph: def __init__(self, vertices, edges): self.__vertices = vertices self.__edges = edges @property def vertices(self): return self.__vertices @vertices.setter def vertices(self, value): self.__vertices = value @property def edges(self): return self.__edges @edges.setter def edges(self, value): self.__edges = value def kosajaru(self): # 第一步 DFS计算时间 time_stack = self.dfs_time_stack() # 创建逆图 reversed_edges = [None] * len(self.__edges) # 遍历原图,生成逆图 for from_vertex, neighbours in enumerate(self.__edges): for to_vertex in neighbours: if reversed_edges[to_vertex] is None: reversed_edges[to_vertex] = [] reversed_edges[to_vertex].append(from_vertex) # 打印一下逆图 reversed_graph = UnweightedGraph(self.__vertices, reversed_edges) # 对逆图进行DFS # scc的列表,是一个二维数组,里面有多个scc,每个scc是一个list return UnweightedGraph.dfs_scc(time_stack, reversed_edges) white = 0 gray = 1 black = 2 def dfs_time_stack(self): # 着色 colors = [UnweightedGraph.white for _ in self.__vertices] times = [{'d': None, 'f': None} for _ in self.__vertices] time = 1 time_stack = [] for root, _ in enumerate(self.__vertices): # 其实可以用栈代替 if not colors[root] == UnweightedGraph.white: continue # 这里的i是森林的一个根 times[root]['d'] = time time += 1 stack = [root] while len(stack) > 0: # 这一步就错了,不要直接弹出 v = stack[-1] if colors[v] == UnweightedGraph.black: stack.pop() times[v]['f'] = time time += 1 time_stack.append(v) else: colors[v] = UnweightedGraph.gray for neighbor in self.__edges[v]: if colors[neighbor] == UnweightedGraph.white: times[neighbor]['d'] = time time += 1 stack.append(neighbor) colors[neighbor] = UnweightedGraph.gray colors[v] = UnweightedGraph.black return time_stack @staticmethod def dfs_scc(time_stack, edges): # 着色 visited = [False for _ in time_stack] # d代表discover f代表finish,发现时间戳和完成时间戳 scc_list = [] for root in reversed(time_stack): # 其实可以用栈代替 if visited[root]: continue # 这里的i是森林的一个根 stack = [root] scc = [] while len(stack) > 0: v = stack.pop() if visited[v]: continue for neighbor in edges[v]: if not visited[neighbor]: stack.append(neighbor) visited[v] = True scc.append(v) scc_list.append(scc) return scc_list def to_dot(self): s = 'digraph s {\n' for v in self.__vertices: s += f'"{v}";\n' for i, e in enumerate(self.__edges): for t in e: s += f'\"{self.__vertices[i]}\"->"{self.__vertices[t]}";\n' s += '}\n' return s
-
kosaraju算法
2020-08-23 17:55:08深入浅出的用Kosaraju算法计算得到有向图的强连通分量数kosaraju算法用来求有向图的强连通分量
我们需要提前知道的知识
什么是强连通
有向图中,从v可以到达w, 从w也可以到达v,那么称v和w是强连通的
如果一副图中任意两点强连通,这幅图也是强连通的。
强连通分量
- 本图强连通分量为3
- 本图强连通分量为4
- 本图强连通分量为4
无向图的连通分量怎么计算
kosaraju算法不过是在下面的基础上增加了几行,
package Algorithm; public class CC { private int count;//连通分量个数 private boolean[] marked;//标记是否访问过 private int[] id;//id[v]=1,说明顶点v在分量1里面 public CC(Graph g){ marked=new boolean[g.V()]; id=new int[g.V()]; for (int i = 0; i <g.V(); i++) { if(!marked[i]){ dfs(g,i); count++; } } } private void dfs(Graph g,int v){ marked[v]=true; id[v]=count; for (int w:g.adj(v)){ if(!marked[w]) dfs(g,w); } } public boolean connected(int v,int w){ return id[v]==id[w]; } public int count(){ return count; } public int id(int v){ return id[v]; } }
什么是顶点的逆后序排列
调用dfs时,将顶点放在栈里面即可。
package Algorithm.DirectedGraph; import java.util.LinkedList; public class DF_Order { private LinkedList<Integer> reversePost;//栈 private boolean[] marked;//标记是否访问过 private int vNum;//顶点数目 public DF_Order(DiGraph g){ vNum=g.getV(); reversePost=new LinkedList<>(); marked=new boolean[g.getV()]; for (int i = 0; i < g.getV(); i++) { if(!marked[i]){ dfs(g,i); } } } private void dfs(DiGraph g, int i) { marked[i]=true; for(int w:g.adj(i)){ if(!marked[w]){ dfs(g,w); } } reversePost.push(i); } public Iterable<Integer> reversePost(){ return reversePost; } }
什么是反向图
将有向图的边的指向反过来即可。
反向图:
kosaraju算法
步骤
- 将图反向
- 得到反向图的逆后序排列
- 用逆后序的顺序遍历原图
package Algorithm.DirectedGraph; public class KosarajuCC { private int[] id; private int count; private boolean[] marked; public KosarajuCC(DiGraph graph){ marked=new boolean[graph.getV()]; id=new int[graph.getV()]; DF_Order order = new DF_Order(graph.reverse()); for (int x:order.reversePost()){ if(!marked[x]) { dfs(graph,x); count++; } } } private void dfs(DiGraph graph, int v) { marked[v]=true; id[v]=count; for (int x:graph.adj(v)){ if(!marked[x]) dfs(graph,x); } } public boolean stronglyConnected(int v,int w){ return id[v]==id[w]; } public int getCount(){ return count; } }
对于该算法的理解
我们从哪一个顶点进行DFS呢?
按照顶点顺序:0 1 2 3 4 5
那么0 1是一个强连通分量
2 3 4 5是一个强连通分量,显然是不对的。
按照逆后序就是: 0 1 4 5 2 3
那么0 1是一个强连通分量
4 5是一个强连通分量
2 3是一个强连通分量
先访问0 1 4 5,在进行2 3,那么2通向1或4 的道路被封死了。
用这个反向图的逆后序去深度优先搜索(正向)图,可以发现每次递归都是在同一个强连通分量之中。
-
如图
逆后序得到:0 1 4 5 2 3 -
.
逆后序得到 4 5 2 3 0 1 -
.
逆后序得到 2 3 0 1 4 5
无向图,有向图的图结构
package Algorithm.DirectedGraph; import Algorithm.Bag; public class DiGraph { private int v; private int e; private Bag<Integer>[] adj; public DiGraph(int v,int e){ this.v=v; this.e=e; adj=(Bag<Integer>[]) new Bag[v]; for (int i = 0; i < v; i++) { adj[i]=new Bag<Integer>(); } } public DiGraph(int v){ this.v=v; adj=(Bag<Integer>[]) new Bag[v]; for (int i = 0; i < v; i++) { adj[i]=new Bag<Integer>(); } } public int getV(){return v;}; public int getE(){return e;}; public void add(int v,int w){ adj[v].add(w); } public Iterable<Integer> adj(int v){ return adj[v]; } public DiGraph reverse(){ DiGraph diGraph = new DiGraph(v); for (int i = 0; i < v; i++) { for (int x:adj(i)){ diGraph.add(x,i); } } return diGraph; } }
//无向图 package Algorithm; public class Graph { private int V; private int E; private Bag<Integer>[] adj;//邻接表 public Graph(int v,int e) { this.V = v; this.E=e; adj=(Bag<Integer>[]) new Bag[v]; for (int i = 0; i < V; i++) { adj[i]=new Bag<Integer>(); } } public Graph(int v){ this.V=v; adj=(Bag<Integer>[]) new Bag[v]; for (int i = 0; i < v; i++) { adj[i]=new Bag<Integer>(); } } public int V(){ //返回定点数 return V; } public int E(){ //返回边数 return E; } public void addEdge(int v,int w){ //添加一条边 adj[v].add(w); adj[w].add(v); E++; } public Iterable<Integer> adj(int v){ //和v相邻的所有顶点。只是输入的时候,不包含计算得出的所有 return adj[v]; } }
package Algorithm; import java.util.Iterator; public class Bag<Item> implements Iterable<Item> { @Override public Iterator<Item> iterator() {//实现iterable,才可以foreach //iterator 迭代器 return new ListIterator(); } private class ListIterator implements Iterator<Item> { private node current=first; @Override public boolean hasNext() { return current!=null; } @Override public Item next() { Item item=current.val; current=current.next; return item; } } private class node{ Item val; node next; } private int n=0; private node first; public void add(Item item){ node oldfirst=first; first=new node(); first.val=item; first.next=oldfirst; n++; } public boolean isEmpty(){ return first==null; } public int size(){ return n; } }
参考自:
- 知乎:https://www.zhihu.com/question/58926821
- 算法第四版(橙色)
- 本图强连通分量为3
-
【数据结构与算法python】强连通分治算法-Kosaraju算法
2020-09-11 09:10:55在生活中,可以发现在某些图中,如web底层结构、人际关系网,在图中可以发现高度聚集节点群的算法, 即寻找**“强连通分支Strongly Connected Components”算法。 强连通分支, 定义为图G的一个子集C,C中的任意两个...1、引入
在生活中,可以发现在某些图中,如web底层结构、人际关系网,在图中可以发现高度聚集节点群的算法, 即寻找**“强连通分支Strongly Connected Components”算法。
强连通分支, 定义为图G的一个子集C,C中的任意两个顶点v,w之间都有路径来回**,即(v,w)(w,v)都是C的路径,而且C是具有这样性质的最大子集。
下图是具有3个强连通分支的9顶点有向图
一旦找到强连通分支, 可以据此对图的顶点进行分类, 并对图进行化简。
2、功能分析
(1)转置概念
在用深度优先搜索来发现强连通分支之前, 先熟悉一个概念: Transposition转置,一个有向图G的转置GT,定义为将图G的所有边的顶点交换次序,如将(v,w)转换为(w,v),可以观察到图和转置图在强连通分支的数量和划分上,是相同的。
(2)Kosaraju算法思路
- 首先, 对图G调用DFS算法, 为每个顶点计算“结束时间”;
- 然后, 将图G进行转置, 得到GT;
- 再对GT调用DFS算法, 但在dfs函数中,对每个顶点的搜索循环里, 要以顶点的“结束时间”倒序的顺序来搜索
- 最后, 深度优先森林中的每一棵树就是一 个强连通分支
以一个例子为例,来阐述以上算法
Step1:第一趟DFS
Step2:转置后第二趟DFS
Step3:深度优先森林结果
-
Kosaraju算法求强连通分量
2021-05-11 09:19:52Kosaraju算法 该算法旨在得到深度优先后续排列后的递归探索中,每次调用DFS的所有顶点都属于同一强联通分量。所以可以这么理解:当递归进入一个强联通分量时,把他锁死在这个强联通分量中(即不能从该强联通分量中... -
【图论】Kosaraju算法详解
2020-04-11 00:38:34讲 Kosaraju 算法之前要先知道什么是强连通分量(SCC) 强连通分量 对于一个有向图顶点的子集 S ,如果在 S 内取两个顶点 u 和 v ,都能找到一条从 u 到 v 的路径,那么就称 S 是强连通的。 如果在强连通的顶点... -
Algorithm第四版算法 C++实现(十七)——kosaraju算法(计算强连通分支量)
2021-08-13 13:25:56kosaraju算法通过两次DFS来实现,第一次对有向图D进行搜索,并标记顺序;第二次对有向图D的反图(见有向图的构造revers方法)进行搜索。从构造函数进入dfs函数之后,只要不退出则说明之后遍历的都是连通的。 class ... -
强连通分量(Kosaraju算法)
2019-10-04 15:23:07算法思想: 在一个有向图中,我们一定可以找到这样一个合理顺序,使得我们只需要按照这个顺序进行dfs遍历,那么每一次的dfs就可以使我们得到一个scc。合理顺序参考https://www.cnblogs.com/nullzx/p/6437926.html ... -
求强连通分量的Kosaraju算法和Tarjan算法的比较 by ljq.doc
2020-03-02 01:33:45求强连通分量的Kosaraju算法和Tarjan算法的比较 一定义 在有向图中如果两个顶点vi,vj间有一条从vi到vj的有向路径同时还有一条从vj到vi的有向路径则称两个顶点强连通(strongly connected)如果有向图的每两个顶点都强... -
图论- 图的连通性- Kosaraju 算法.rar
2021-09-16 22:49:46图论- 图的连通性- Kosaraju 算法.rar -
Python:实现SCC的Kosaraju算法(附完整源码)
2022-07-28 12:39:03Python:实现SCC的Kosaraju算法(附完整源码) -
C#,图论与图算法,寻找图强连通分量(SCC,Strongly Connected Components)的Kosaraju算法与源代码
2022-06-20 09:47:51S. Rao KosarajuStrongly Connected Components In this tutorial, you ... Also, you will find working examples of Kosaraju's algorithm in C, C++, Java and Python.A strongly connected component is the portio -
Kosaraju算法(强连通分量分解)
2018-08-12 21:44:39对于一个有向图顶点的子集S,如果在S内取两个顶点u和v,都能找到一条从u到v的路径,那么就称S是强连通的。如果在强连通的顶点集合S中加入其他任意顶点集合后,它都不再是强连通的,那么就称S是原图的一个强连通分量... -
Kosaraju算法之直观理解
2020-03-29 18:51:32Kosaraju算法用来解决有向图的连通性问题,算法的基本步骤: 1.对一幅有向图G,计算它的反向图GR的逆后序排列(一次dfs)。 2.按由1计算得到的顺序对G进行dfs操作,来访问所有未被标记的顶点。 3.在构造函数中,... -
Kosaraju 算法
2016-09-18 00:18:00在计算科学中,Kosaraju的算法(又称为–Sharir Kosaraju算法)是一个线性时间(linear time)算法找到的有向图的强连通分量。它利用了一个事实,逆图(与各边方向相同的图形反转, transpose graph)有相同的强连通...