阅读此篇之前,需要了解图论的基础知识 。
拓扑排序可以解决我们有优先级限制 下的任务调度问题中的排序问题,或是类似Java中类的继承关系。
先来看下概念:
拓扑排序:给定一个有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素。(或是说明无法做到这一点)
通常在进行任务调度类问题需要3步走:
指明任务和优先级。(确定顶点和有向边)
不断检测并去除有向图中的所有环,以确保存在可执行的方案。(构建有向图,检测环)
使用拓扑排序解决问题。(拓扑排序)
有向图的构建,已经在图论的基础知识 中详细阐述了,接下来就是如何解决环的问题。
有向环 如果任务x必须在y之前完成,y必须在z之前完成,z必须在x之前完成,出现了有向环,那么这个问题就是无解的。
检测有向环 通过深度优先搜索DFS可以很简单的完成这个检测。
当我们进行DFS时,维护的递归调用栈表示的是系统正在遍历的有向路径,一旦我们找到了一条路径v -> w
已经存在于栈中,那么我们就找到了一个环。
相较于有向图而言,使用一个boolean
数组onStack[]
来保存遍历过的路径,作为检测依据。并用一个Stack
来保存环。
DirectedCycle 有向环的检测 DirectedCycle.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class DirectedCycle { private boolean [] marked; private int [] edgeTo; private Stack<Integer> cycle; private boolean [] onStack; public DirectedCycle (Digraph G) { onStack = new boolean [G.V()]; marked = new boolean [G.V()]; edgeTo = new int [G.V()]; for (int v = 0 ; v < G.V(); v++) if (!marked[v]) dfs(G, v); } private void dfs (Digraph G, int v) { onStack[v] = true ; marked[v] = true ; for (int w : G.adj(v)) { if (hasCycle()) return ; else if (!marked[w]) { edgeTo[w] = v; dfs(G, w); } else if (onStack[w]) { cycle = new Stack<>(); for (int x = v; x != w; x = edgeTo[x]) cycle.push(x); cycle.push(w); cycle.push(v); } } onStack[v] = false ; } public boolean hasCycle () { return cycle != null ; } public Iterable<Integer> cycle () { return cycle; } }
拓扑排序数据结构 基于DFS的逆后序排序 拓扑排序所需要的顶点顺序,其实只需要将DFS算法每次访问的顶点(DFS递归访问时,每个顶点只访问一次)采取逆后序排序即可。(压入栈中)
原理:对于v -> w
,在进行DFS时递归访问,总是dsf(w)
先于dsf(v)
完成,故将完成dfs()
访问一个顶点时压入栈即可完成v -> w
顺序。
DepthFirstOrder 基于DFS的逆后序排序 DepthFirstOrder.java 1 2 3 4 5 6 7 8 9 10 11 12 13 public class DepthFirstOrder { ··· private Stack<Integer> reversePost; private void dfs (Digraph G, int v) { ··· reversePost.push(v); } public Iterable<Integer> reversePost () { return reversePost; } }
拓扑排序实现 非常简单,就是检测环,如果是有向无环图DAG
,则返回顶点逆后序排序即可。
Topological 拓扑排序 Topological.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Topological { private Iterable<Integer> order; public Topological (Digraph G) { DirectedCycle cycleFinder = new DirectedCycle(G); if (!cycleFinder.hasCycle()) { DepthFirstOrder dfs = new DepthFirstOrder(G); order = dfs.reversePost(); } } public Iterable<Integer> order () { return order; } }
性能:
这是由于每次拓扑排序时,会进行两次DFS。
第一次DFS遍历有向图确保无环,此时V+E。第二次DFS产生逆后序排序,此时V+E。两次都访问了顶点和边,故与V+E成正比。
强连通分量
探寻两点之间是否互相可达,是有向图中的另一个重要部分——强连通分量。
应用 这是一种非常重要的抽象,可以理解为几个相互关联的部分(顶点)是否是一种类型的。比如程序员对于程序模块的划分、写书的作者对于话题或者书中知识点的分类、生物学食物链中的能量流动是否存在相互关系、巨型互联网中网页网站的分模块处理等等。
实现 想实现一个平方级的算法并不难,但是对于大型数据而言是不可接受的(大型稀疏图)。 这里介绍一个Kosaraju算法
巧妙的实现。
给定的有向图G,使用DepthFirstOrder
来计算G的反向图Gr的逆后序排列,得到order
。
在G的构造函数中DFS,但是DFS访问的顶点顺序根据order
进行。
实现简单,但是理解比较难。
命题:用Kosaraju算法
处理G,其构造函数中的每次递归都在同一个强连通分量中。 证明:
首先证明每个和s强连通的顶点v都会在构造函数dfs(G,s)
中访问到。(这个不难理解,反证法即可)
再证明v和s必然是强连通的。设v为dfs(G,s)
中到达的某个顶点,那么G中必然存在s到v的路径。故只要证明存在v到s的路径即可。等价于证明Gr中存在s到v的路径。其中Gr中已经含有v到s的路径,故对Gr进行DFS时,dfs(G,v)
必然先于dfs(G,s)
结束,并且是在dfs(G,s)
之后调用的,这个顺序是由DFS的递归访问导致的。所以此时说明了存在一条s到v的路径。
KosarajuSCC 检测强连通分量 KosarajuSCC.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class KosarajuSCC { private boolean [] marked; private int [] id; private int count; public KosarajuSCC (Digraph G) { marked = new boolean [G.V()]; id = new int [G.V()]; DepthFirstOrder order = new DepthFirstOrder(G); for (int s : order.reversePost()) { if (!marked[s]) { dfs(G, s); count++; } } } private void dfs (Digraph G, int v) { marked[v] = true ; id[v] = count; for (int w : G.adj(v)) if (!marked[w]) dfs(G, w); } public boolean stronglyConnected (int v, int w) { return id[v] == id[w]; } ··· }
其中有向图的反转实现如下:
Digraph 有向图反转 Digraph.java 1 2 3 4 5 6 7 8 9 10 11 public class Digraph { ··· public Digraph reverse () { Digraph R = new Digraph(V); for (int v = 0 ; v < V; v++) for (int w : adj[v]) R.addEdge(w, v); return R; } ··· }
性能:
预处理
查询
参考文献:《算法导论》 《Algorithms, 4th Edition》