概念
将有向图中的顶点以线性方式进行排序,是指对于任何连接自顶点u到顶点v的有向边uv,在最后的排序结果中,顶点u总是出现在顶点v的前面。
例如,图的顶点可能代表将要被执行的任务,边代表一个任务必须在另一个任务之前执行。在该应用场景中,一个拓扑排序结果就是一个有效的任务序列。
前置条件
一个有向图能进行拓扑排序的充要条件是,它是一个有向无环图,Directed Acyclic Graph。
任意DAG至少有一个拓扑排序结果,并且已知算法可以在线性时间内为任意DAG产生一个拓扑排序结果。
示例
拓扑排序的一个典型应用是,基于任务之间的依赖关系,安排一个执行序列。任务用顶点表示,从顶点x指向顶点y的边表示任务x必须在任务y开始之前完成。一个拓扑排序结果可以给出执行这些任务的一个顺序。
在计算机科学中,这样的应用场景有:指令调度,确定makefile中的编译任务顺序,数据序列化,链接器中的符号依赖。
左图可能有以下有效的拓扑排序结果: 5,7,3,11,8,2,9,10(从左到右,从上到下) 3,5,7,8,11,2,9,10(小编号优先) 5,7,3,8,11,10,9,2(最少边数优先) 7,5,11,3,10,8,9,2(大编号优先) 5,7,11,2,3,8,9,10(从上到下,从左到右) |
算法
通常,拓扑排序算法的时间复杂度是顶点数和边数之和的线性关系。O(|V|+|E|)。
1. Kahn算法
首先,找到起始结点(start nodes)列表,并指导它们放入一个集合S中。起始结点是指没有入边的结点,一个非空无环图中至少有一个这样的结点。
空链表L,将用于存放排好序的元素。
然后:
while S is non-empty do remove a node n from S add n to tail of L for each node m with an edge e from n to m do remove edge e from the graph if m has no other incoming edges then insert m into S if graph has edges then return error (graph has at least one cycle) else return L(a topologically sorted order) |
每次从S中取出一个结点n,放入链表L中,然后遍历从该结点引出的所有边,从图中移除这条边,同时获取该边的另一个顶点,如果该顶点的入度在减去本边后为0,则也将它放入集合S中。然后循环从集合S中取出一个结点n……
如果输入的图是一个有向无环图,一条路径将会包含在列表L中(不一定唯一)。否则,该图必然含有至少一个环,无法完成拓扑排序。
因为排序结果不唯一,所以集合S的数据结构可以简单的是一个Set,或一个队列queue,或一个栈stack。根据结点n从集合S中取出的顺序,将会产生不同的方案。
【基于邻接矩阵的代码实现】
由上述分析可知,本算法需要维护一个集合S,图上的所有边和顶点的关系,每个顶点的入度数,排序结果存储在一个列表L中,最终根据剩余边数来判断是否存在环。因此,需要的数据结构有:邻接矩阵int[][] gragh(表示图中的边关系),入度数数组int[] indegree,入度为0的顶点集Set set,剩余边数edges。通过维护gragh和edges来表示剩余的边,维护入度数数组来判断某个顶点是否可加入Set中。如果最终edges大于0,说明存在环。
【基于邻接表的代码实现】
使用邻接矩阵进行拓扑排序,程序实现较为简单,但是效率不高。
邻接表可以使入度为0的顶点的操作简化,从而提高效率。
在邻接表中,为了便于检查每个顶点的入度,可在顶点表中增加一个入度数属性。
2. 深度优先搜索
以任意顺序遍历图中的每个结点,直至遇到一个已经访问过的结点或者遇到一个没有出边的结点。
while there are unmarked nodes do select an unmarked node n visit(n) function visit(node n) if n has a temporary mark then stop(not a DAG) if n is not marked then mark n temporarily for each node m with an edge from n to m do visit(m)//递归遍历 mark n permanently unmark n temporarily add n to head of L |
每个结点n放入链表L之前,先考虑所有其他依赖于n的结点(n的所有后代)。这可以保证,每当向L添加结点n时,所有依赖n的结点已经在链表L中了:通过在访问结点n时递归调用visit()。因为每条边和每个结点都会被访问一次,算法的时间复杂度为线性的。
注意,当有向图无环时,由图中某点出发进行深度优先搜索遍历,最早被放入结果集的顶点一定是出度为0的顶点,一般是拓扑排序中的最后一个顶点。因此,用栈来存储结果会比较自然。
需要额外记录每个结点的状态(是否访问过),用于判断是否存在环,以及是否已遍历完毕。
【基于邻接矩阵的代码实现】
所以,最终结果是:D,E,A,C,B,G,F.
【基于邻接表的代码实现】
基本和基于邻接表差不多,都是用递归来实现。只是由于数据结构不同,所以visit方法有所变化。
应用
1. 最短路径搜索
拓扑排序也可以用于快速计算一个带权值的有向无环图中的最短路径。
用V表示该图中的顶点列表,按拓扑排序的顺序。以下算法用于计算源顶点S到其他顶点的最短路径。
Let d be an array of the same length as V; this will hold the shortest-path distances from s. Set d[s] = 0, all other d[u] = ∞. 数组d用于记录从源顶点到对应点的最短距离,初始化。 Let p be an array of the same length as V, with all elements initialized to nil. Each p[u] will hold the predecessor of u in the shortest path from s to u. 数组p用于记录对应点的前驱,p[u]表示从s到u的最短路径中u的前驱。 Loop over the vertices u as ordered in V, starting from s: For each vertex v directly following u (i.e., there exists an edge from u to v): Let w be the weight of the edge from u to v. 按顺序遍历V中的顶点u,从源顶点S开始:对于任意从u出发可直接到达的顶点v,其权重记为w Relax the edge: if d[v] > d[u] + w, set d[v] ← d[u] + w, p[v] ← u. 松弛边:如果d[v]>d[u]+w,则d[v]=d[u]+w, p[v]=u。 |
在一个含有n个顶点和m条边的图中,该算法的时间复杂度为O(n+m)。线性。
另一个算法搜索与顶点集V中的顶点v相连的所有边,而不仅仅是从该点出发的边。这样的好处是可以作为一个在线算法,如果新的顶点在运行过程中被添加到顶点列表尾部,该算法也可以输出最短路径。