某些编程语言(如haskell)允许模块之间存在循环依赖关系。由于编译器需要知道在编译一个模块时导入的所有模块的所有定义,如果某些模块相互导入或发生任何其他类型的循环,它通常需要做一些额外的工作。在这种情况下,编译器可能无法像没有导入周期的模块那样优化代码,因为可能尚未分析导入的函数。通常只有一个循环的一个模块必须以这种方式编译,因为二进制对象没有依赖性。我们称之为模块 loop-breaker
特别是如果导入周期是交错的,那么在编译由数百个模块组成的大项目时,如何最大限度地减少环路断路器的数量是很有趣的。
是否有算法给出一组依赖性输出最少数量的模块需要编译为循环断路器才能成功编译程序?
我试着澄清一下这个例子中我的意思。
考虑一个包含四个模块A
,B
,C
和D
的项目。这是这些模块之间的依赖关系列表,条目X y
表示 y
取决于x
:
A C A D B A C B D B
可视化为ASCII图的相同关系:
D ---> B ^ / ^ | / | | / | | L | A ---> C
此依赖关系图中有两个周期:ADB
和ACB
。为了打破这些循环,可以将模块C
和D
编译为循环断路器。显然,这不是最好的方法。将A
编译为循环断开器完全足以破坏两个循环,并且需要编译一个较少的模块作为循环断路器。
答案 0 :(得分:16)
这是NP-hard(和APX-hard)问题,称为minimum feedback vertex set。正如我对您的应用所期望的那样,当没有长的简单周期时,由Demetrescu and Finocchi (pdf, Combinatorial Algorithms for Feedback Problems in Directed Graphs (2003)")引起的近似算法很有效。
答案 1 :(得分:4)
以下是如何在Python中执行此操作:
from collections import defaultdict
def topological_sort(dependency_pairs):
'Sort values subject to dependency constraints'
num_heads = defaultdict(int) # num arrows pointing in
tails = defaultdict(list) # list of arrows going out
for h, t in dependency_pairs:
num_heads[t] += 1
tails[h].append(t)
ordered = [h for h in tails if h not in num_heads]
for h in ordered:
for t in tails[h]:
num_heads[t] -= 1
if not num_heads[t]:
ordered.append(t)
cyclic = [n for n, heads in num_heads.iteritems() if heads]
return ordered, cyclic
def is_toposorted(ordered, dependency_pairs):
'''Return True if all dependencies have been honored.
Raise KeyError for missing tasks.
'''
rank = {t: i for i, t in enumerate(ordered)}
return all(rank[h] < rank[t] for h, t in dependency_pairs)
print topological_sort('aa'.split())
ordered, cyclic = topological_sort('ah bg cf ch di ed fb fg hd he ib'.split())
print ordered, cyclic
print is_toposorted(ordered, 'ah bg cf ch di ed fb fg hd he ib'.split())
print topological_sort('ah bg cf ch di ed fb fg hd he ib ba xx'.split())
运行时与边数(依赖关系)成线性比例。
该算法围绕一个名为num_heads的查找表进行组织,该查找表保持计数前任的数量(传入箭头)。在ah bg cf ch di ed fb fg hd he ib
示例中,计数为:
node number of incoming edges
---- ------------------------
a 0
b 2
c 0
d 2
e 1
f 1
g 2
h 2
i 1
该算法通过“visting”没有前任的节点来工作。例如,节点a
和c
没有传入边,因此首先访问它们。
访问意味着从图中输出和删除节点。当访问一个节点时,我们遍历其后继者并将其传入计数减1。
例如,在访问节点a
时,我们转到其后继h
将其传入计数减1(以便h 2
变为h 1
。
同样,在访问节点c
时,我们会遍历其后继者f
和h
,将其计数减一(以便f 1
变为f 0
并且h 1
变为h 0
)。
节点f
和h
不再有传入边缘,因此我们重复输出它们并从图形中删除它们直到所有节点都被访问过程。在示例中,访问顺序(拓扑排序):
a c f h e d i b g
如果num_heads到达没有没有传入边的节点的状态,那么这意味着有一个循环无法进行拓扑排序并退出算法。