拓扑排序
在计算机科学中,有向图的拓扑排序或拓扑排序是其顶点的线性排序,使得对于从顶点 u 到顶点 v 的每个有向边 uv,u 在排序中都位于 v 之前。例如,图的顶点可以表示要执行的任务,边缘可以表示一个任务必须在另一个任务之前执行的约束;在此应用程序中,拓扑排序只是任务的有效序列。准确地说,拓扑排序是一种图形遍历,其中每个节点 v 仅在访问其所有依赖项后才被访问。当且仅当图没有有向循环时,即如果它是有向无环图 (DAG),拓扑排序是可能的。任何 DAG 都至少有一个拓扑排序,并且已知算法以线性时间构造任何 DAG 的拓扑排序而闻名。拓扑排序有许多应用,特别是在反馈弧集等排序问题中。即使 DAG 的组件断开连接,也可以进行拓扑排序。
示例[编辑]
拓扑排序的规范应用是根据作业或任务的依赖关系来调度一系列作业或任务。作业由顶点表示,如果作业 x 必须在作业 y 开始之前完成(例如,洗衣服时,洗衣机必须在我们将衣服放入烘干机之前完成),则从 x 到 y 有一个边。然后,拓扑排序给出执行作业的顺序。拓扑排序算法的密切相关的应用在 1960 年代初首次在项目管理中调度的 PERT 技术的背景下进行研究。[1] 在此应用程序中,图形的顶点表示项目的里程碑,边表示必须在一个里程碑和另一个里程碑之间执行的任务。拓扑排序构成了线性时间算法的基础,用于查找项目的关键路径,项目的关键路径是控制整个项目进度长度的一系列里程碑和任务。
在计算机科学中,这种类型的应用出现在指令调度、在电子表格中重新计算公式值时公式单元格评估的排序、逻辑合成、确定要在 makefile 中执行的编译任务的顺序、数据序列化以及解析链接器中的符号依赖关系。它还用于决定在数据库中加载带有外键的表的顺序。
左图显示有许多有效的拓扑排序,包括:
|
算法[编辑]
通常的拓扑排序算法在节点数加上边数方面具有运行时间线性,渐近地,{\displaystyle O(\left|{V}\right|+\left|{E}\right|).}
卡恩算法[编辑]
其中一种算法首先由Kahn(1962)描述,它的工作原理是按照与最终拓扑排序相同的顺序选择顶点。[2] 首先,找到一个没有传入边的“开始节点”列表,并将它们插入到集合 S 中;非空无环图中必须至少存在一个这样的节点。然后:
L ← Empty list that will contain the sorted elements
S ← Set of all nodes with no incoming edge
while S is not empty do
remove a node n from S
add n to 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)
如果图形是 DAG,则解决方案将包含在列表 L 中(解决方案不一定是唯一的)。否则,图必须至少有一个循环,因此拓扑排序是不可能的。
反映结果排序的非唯一性,结构 S 可以只是一个集合、队列或堆栈。根据节点 n 从集合 S 中删除的顺序,将创建不同的解决方案。卡恩算法的一种变体在字典上打破了联系,形成了用于并行调度和分层图形绘制的科夫曼-格雷厄姆算法的关键组成部分。
深度优先搜索[编辑]
拓扑排序的另一种算法基于深度优先搜索。该算法以任意顺序循环遍历图的每个节点,启动深度优先搜索,当它命中自拓扑排序开始以来已经访问过的任何节点或节点没有传出边(即叶节点)时终止:
L ← Empty list that will contain the sorted nodes
while exists nodes without a permanent mark do
select an unmarked node n
visit(n)
function visit(node n)
if n has a permanent mark then
return
if n has a temporary mark then
stop (graph has at least one cycle)
mark n with a temporary mark
for each node m with an edge from n to m do
visit(m)
remove temporary mark from n
mark n with a permanent mark
add n to head of L
每个节点 n 只有在考虑了依赖于 n 的所有其他节点(图中 n 的所有后代)之后才会附加到输出列表 L 之前。具体来说,当算法添加节点 n 时,我们保证所有依赖于 n 的节点都已经在输出列表 L 中:它们被添加到 L 中,要么是在调用访问 n 之前结束的递归调用 visit(),要么是通过对 visit() 的调用,甚至在调用访问 n 之前就开始了。由于每个边和节点都访问一次,因此算法以线性时间运行。这种基于深度优先搜索的算法是Cormen等人(2001)描述的算法;[3]它似乎是在1976年由Tarjan首次在印刷品中描述的。[4]
并行算法[编辑]
在并行随机存取机器上,可以使用多项式数量的处理器在O(log 2 n)时间内构造拓扑排序,将问题置于复杂度类NC2中。[5] 执行此操作的一种方法是重复对给定图的邻接矩阵进行平方,多次对数,使用最小加矩阵乘法和最大化代替最小化。生成的矩阵描述了图形中的最长路径距离。按顶点最长传入路径的长度对顶点进行排序将生成拓扑排序。[6]
分布式内存机器上的并行拓扑排序算法并行化了 DAG 的 Kahn 算法 {\displaystyle G=(V,E)}.[7] 在高层次上,Kahn 的算法反复删除 indegree 0 的顶点,并按照它们被删除的顺序将它们添加到拓扑排序中。由于移除顶点的传出边也会被删除,因此将有一组新的入度为 0 的顶点,其中重复该过程,直到没有顶点。此算法执行{\displaystyle D+1}迭代,其中 D 是 G 中的最长路径。每次迭代都可以并行化,这就是以下算法的思路。
在下文中,假设图分区存储在 p 处理元素 (PE) 上,这些元素被标记为{\displaystyle 0,\dots ,p-1}.每个 PE i 初始化一组局部顶点{\displaystyle Q_{i}^{1}}为 0,其中上部索引表示当前迭代。由于局部集合中的所有顶点{\displaystyle Q_{0}^{1},\dots ,Q_{p-1}^{1}}具有 indegree 0,即它们不相邻,它们可以以任意顺序给出有效的拓扑排序。要为每个顶点分配全局索引,将按以下大小计算前缀总和{\displaystyle Q_{0}^{1},\dots ,Q_{p-1}^{1}}.所以每一步,都有{\textstyle \sum _{i=0}^{p-1}|Q_{i}|}添加到拓扑排序中的顶点。
第一步,PE j 分配指数{\textstyle \sum _{i=0}^{j-1}|Q_{i}^{1}|,\dots ,\left(\sum _{i=0}^{j}|Q_{i}^{1}|\right)-1}到局部顶点{\displaystyle Q_{j}^{1}}.这些顶点在{\displaystyle Q_{j}^{1}}连同其相应的传出边一起被移除。对于每个传出边{\displaystyle (u,v)}端点 v 在另一个 PE 中{\displaystyle l,j\neq l}、消息{\displaystyle (u,v)}被发布到 PE l。毕竟所有顶点{\displaystyle Q_{j}^{1}}被删除,发布的消息将发送到其相应的 PE。每条消息{\displaystyle (u,v)}收到的更新是局部顶点 V 的入度。如果度数降至零,则 v 将{\displaystyle Q_{j}^{2}}.然后开始下一次迭代。
在步骤 k 中,PE j 分配索引{\textstyle a_{k-1}+\sum _{i=0}^{j-1}|Q_{i}^{k}|,\dots ,a_{k-1}+\left(\sum _{i=0}^{j}|Q_{i}^{k}|\right)-1}哪里{\displaystyle a_{k-1}}是步长后处理的顶点总量{\displaystyle k-1}.重复此过程,直到没有要处理的顶点,因此{\textstyle \sum _{i=0}^{p-1}|Q_{i}^{D+1}|=0}.下面是该算法的高级、单程序、多数据伪代码概述。
请注意,本地偏移量的前缀总和{\textstyle a_{k-1}+\sum _{i=0}^{j-1}|Q_{i}^{k}|,\dots ,a_{k-1}+\left(\sum _{i=0}^{j}|Q_{i}^{k}|\right)-1}可以有效地并行计算。
p processing elements with IDs from 0 to p-1
Input: G = (V, E) DAG, distributed to PEs, PE index j = 0, ..., p - 1
Output: topological sorting of G
function traverseDAGDistributed
δ incoming degree of local vertices V
Q = {v ∈ V | δ[v] = 0} // All vertices with indegree 0
nrOfVerticesProcessed = 0
do
global build prefix sum over size of Q // get offsets and total amount of vertices in this step
offset = nrOfVerticesProcessed + sum(Qi, i = 0 to j - 1) // j is the processor index
foreach u in Q
localOrder[u] = index++;
foreach (u,v) in E do post message (u, v) to PE owning vertex v
nrOfVerticesProcessed += sum(|Qi|, i = 0 to p - 1)
deliver all messages to neighbors of vertices in Q
receive messages for local vertices V
remove all vertices in Q
foreach message (u, v) received:
if --δ[v] = 0
add v to Q
while global size of Q > 0
return localOrder
通信成本在很大程度上取决于给定的图形分区。至于运行时,在允许在恒定时间内获取和递减的CRCW-PRAM模型上,该算法在{\textstyle {\mathcal {O}}\left({\frac {m+n}{p}}+D(\Delta +\log n)\right)},其中 D 又是 G 中的最长路径,Δ 是最大度数。[注7]
最短路径查找的应用[编辑]
拓扑排序还可用于通过加权有向无环图快速计算最短路径。设 V 是此类图中按拓扑顺序排列的顶点列表。然后,以下算法计算从某个源顶点到所有其他顶点的最短路径:[3]
- 设 d 是一个与 V 长度相同的数组;这将保持与 S 的最短路径距离。设置 d[s] = 0,所有其他 d[u] = ∞。
- 设 p 是一个与 V 长度相同的数组,所有元素都初始化为 nil。每个 p[u] 将保持 u 的前置体在从 s 到 u 的最短路径中。
- 按照 V 中的顺序循环顶点 u,从 s 开始:
- 对于紧跟在 u 之后的每个顶点 v(即,存在从 u 到 v 的边):
- 设 w 是从 u 到 v 的边的权重。
- 放松边缘:如果 d[v] > d[u] + w,则设置
- d[v] ← d[u] + w,
- p[v] ← u.
- 对于紧跟在 u 之后的每个顶点 v(即,存在从 u 到 v 的边):
等效地:
- 设 d 是一个与 V 长度相同的数组;这将保持与 S 的最短路径距离。设置 d[s] = 0,所有其他 d[u] = ∞。
- 设 p 是一个与 V 长度相同的数组,所有元素都初始化为 nil。每个 p[u] 将保持 u 的前置体在从 s 到 u 的最短路径中。
- 按照 V 中的顺序循环顶点 u,从 s 开始:
- 对于每个顶点 v 到 u(即,存在从 v 到 u 的边):
- 设 w 是从 v 到 u 的边的权重。
- 放松边缘:如果 d[u] > d[v] + w,则设置
- d[u] ← d[v] + w,
- p[u] ← v.
- 对于每个顶点 v 到 u(即,存在从 v 到 u 的边):
在 n 个顶点和 m 条边的图形上,该算法采用 Θ(n + m),即线性时间。[3]
独特性[编辑]
如果拓扑排序具有排序顺序中的所有连续顶点对都由边连接的属性,则这些边在 DAG 中形成有向哈密顿路径。如果存在哈密顿路径,则拓扑排序顺序是唯一的;没有其他顺序尊重路径的边缘。相反,如果拓扑排序不形成哈密顿路径,则 DAG 将具有两个或多个有效的拓扑排序,因为在这种情况下,始终可以通过交换两个未通过边相互连接的连续顶点来形成第二个有效排序。因此,可以线性时间测试是否存在唯一排序,以及是否存在哈密顿路径,尽管哈密顿路径问题的NP硬度对于更一般的有向图(即循环有向图)。[8]
与部分订单的关系[编辑]
拓扑排序也与数学中偏序的线性扩展的概念密切相关。偏序集合只是一组对象以及“≤”不等式关系的定义,满足自反性(x ≤ x)、反对称(如果 x ≤ y 和 y ≤ x 则 x = y)和传递性(如果 x ≤ y 和 y ≤ z,则 x ≤ z)的公理。 总序是一个偏序,其中对于集合中每两个对象 x 和 y,x ≤ y 或 y ≤ x。 总订单在计算机科学中很熟悉,因为比较运算符需要执行比较排序算法。对于有限集合,总阶可以用对象的线性序列来标识,其中每当顺序中的第一个对象先于第二个对象时,“≤”关系为真;比较排序算法可用于以这种方式将总订单转换为序列。偏序的线性扩展是与其兼容的总序,从某种意义上说,如果 x 在偏序中≤ y,则 x 在总序中也≤ y。
可以通过让对象集成为 DAG 的顶点来定义任何 DAG 的部分排序,并将 x ≤ y 定义为 true,对于任何两个顶点 x 和 y,只要存在从 x 到 y 的有向路径;也就是说,只要 y 可以从 X 到达。有了这些定义,DAG 的拓扑顺序与此偏序的线性扩展是一回事。相反,任何部分排序都可以定义为 DAG 中的可达性关系。执行此操作的一种方法是定义一个 DAG,该 DAG 为偏序集中的每个对象都有一个顶点,对于 x ≤ y 的每对对象都有一个边 xy。执行此操作的另一种方法是使用部分排序的传递约简;通常,这会生成具有较少边的 DAG,但这些 DAG 中的可达性关系仍然是相同的偏序。通过使用这些构造,可以使用拓扑排序算法来查找偏序的线性扩展。
与调度优化的关系[编辑]
根据定义,包含优先级图的调度问题的解是拓扑排序的有效解决方案(无论计算机数量如何),但是,拓扑排序本身不足以最佳地解决调度优化问题。Hu 算法是一种流行的方法,用于解决需要优先图并涉及处理时间(目标是最小化所有作业中最大的完成时间)的调度问题。与拓扑排序一样,Hu 算法不是唯一的,可以使用 DFS 求解(通过找到最大路径长度,然后分配作业)。
参见[编辑]
参考资料[编辑]
- ^ Jarnagin,M.P.(1960),测试PERT网络一致性的自动机器方法,技术备忘录编号。K-24/60,弗吉尼亚州达尔格伦:美国海军武器实验室
- ^ Kahn, Arthur B. (1962), “大型网络的拓扑排序”, ACM通讯, 5 (11): 558–562, doi:10.1145/368996.369025, S2CID 16728233
- ^ Jump up to:一个 乙 c 科尔门、托马斯·莱瑟森,查尔斯·里维斯特、罗纳德·Stein, Clifford (2001), “Section 22.4: Topological sort”, Introduction to Algorithms(第2版), MIT Press and McGraw-Hill, pp. 549–552, ISBN 0-262-03293-7
- ^ 塔尔詹、罗伯特·(1976), “边缘不相交生成树和深度优先搜索”, 信息学报, 6 (2): 171–185, doi:10.1007/BF00268499, S2CID 12044793
- ^ 库克、斯蒂芬·(1985), “快速并行算法问题分类”, 信息与控制, 64 (1–3): 2–22, doi:10.1016/S0019-9958(85)80041-3
- ^ 德克尔,埃利泽;纳西米,大卫;Sahni, Sartaj (1981), “Parallel matrix and graph algorithms”, SIAM Journal on Computing, 10 (4): 657–675, doi:10.1137/0210049, MR 0635424
- ^ Jump up to:一个 b 桑德斯,彼得;梅尔霍恩,库尔特;迪茨费尔宾格,马丁;Dementiev, Roman (2019), 顺序和并行算法和数据结构:基本工具箱, 施普林格国际出版社, ISBN 978-3-030-25208-3
- ^ 韦尔内特,奥斯瓦尔多;Markenzon, Lilian (1997), “可约流图的哈密顿问题” (PDF), 论文集: 第17届智利计算机科学学会国际会议, pp. 264–267, doi:10.1109/SCCC.1997.637099, hdl:11422/2585, S2CID 206554481
延伸阅读[编辑]
- D. E. Knuth,《计算机编程的艺术》,第 1 卷,第 2.2.3 节,它给出了用于部分排序的拓扑排序的算法,以及简要历史。