检测有向图内所有周期的最有效算法是什么?
我有一个有向图,表示需要执行的作业计划,作业是一个节点,而依赖项是一个边缘。我需要检测此图中导致循环依赖性的循环的错误情况。
检测有向图内所有周期的最有效算法是什么?
我有一个有向图,表示需要执行的作业计划,作业是一个节点,而依赖项是一个边缘。我需要检测此图中导致循环依赖性的循环的错误情况。
Answers:
Tarjan的强连接组件算法具有O(|E| + |V|)
时间复杂性。
有关其他算法,请参阅Wikipedia上的牢固连接的组件。
O(|E| + |V|)
。使用白色(从未访问过),灰色(已访问当前节点但尚未访问所有可访问节点)和黑色(已访问所有可访问节点以及当前节点已访问)颜色编码,如果一个灰色节点找到了另一个灰色节点,则我们一个周期。[几乎是我们在Cormen的算法书中所拥有的]。想知道“ Tarjan算法”是否比这种DFS有任何好处!!
鉴于这是一份工作时间表,我怀疑您有时会进行排序它们按建议的执行顺序。
如果是这样,则拓扑排序实现无论如何都可以检测到周期。UNIX tsort
当然可以。我认为,因此有可能在tsorting的同时而不是在单独的步骤中更有效地检测周期。
因此问题可能变成“我如何最有效地进行排序”,而不是“我如何最有效地检测循环”。答案可能是“使用图书馆”,但未能获得以下维基百科文章:
包含一种算法的伪代码,以及来自Tarjan的另一种算法的简短描述。两者都有O(|V| + |E|)
时间复杂性。
最简单的方法是对图形进行深度优先遍历(DFT)。
如果图具有n
顶点,则这是O(n)
时间复杂度算法。由于您可能必须从每个顶点开始进行DFT,因此总复杂度变为O(n^2)
。
您必须维护一个堆栈,该堆栈包含当前深度优先遍历的所有顶点,并且其第一个元素为根节点。如果您在DFT期间遇到了已经在堆栈中的元素,那么您就有一个循环。
O(n)
在建议检查堆栈以查看它是否已包含访问节点时?扫描堆栈会增加O(n)
运行时间,因为它必须扫描每个新节点上的堆栈。O(n)
如果您标记访问的节点,则可以实现
根据Cormen等人的引理22.11,算法简介(CLRS):
当且仅当深度优先搜索G不产生后边缘时,有向图G是非循环的。
在几个答案中已经提到了这一点。在这里,我还将基于CLRS的第22章提供一个代码示例。示例图如下所示。
CLRS的深度优先搜索伪代码为:
在CLRS图22.4中的示例中,该图由两个DFS树组成:一个由节点u,v,x和y组成,另一个由节点w和z组成。每棵树都包含一个后边缘:一个从x到v,另一个从z到z(自环)。
关键的实现是,当在DFS-VISIT
函数中遍历的邻居v
时u
,在节点上遇到节点时会遇到后边缘。GRAY
颜色。
以下Python代码是对CLRS伪代码的改编,if
其中添加了用于检测周期的子句:
import collections
class Graph(object):
def __init__(self, edges):
self.edges = edges
self.adj = Graph._build_adjacency_list(edges)
@staticmethod
def _build_adjacency_list(edges):
adj = collections.defaultdict(list)
for edge in edges:
adj[edge[0]].append(edge[1])
return adj
def dfs(G):
discovered = set()
finished = set()
for u in G.adj:
if u not in discovered and u not in finished:
discovered, finished = dfs_visit(G, u, discovered, finished)
def dfs_visit(G, u, discovered, finished):
discovered.add(u)
for v in G.adj[u]:
# Detect cycles
if v in discovered:
print(f"Cycle detected: found a back edge from {u} to {v}.")
# Recurse into DFS tree
if v not in finished:
dfs_visit(G, v, discovered, finished)
discovered.remove(u)
finished.add(u)
return discovered, finished
if __name__ == "__main__":
G = Graph([
('u', 'v'),
('u', 'x'),
('v', 'y'),
('w', 'y'),
('w', 'z'),
('x', 'v'),
('y', 'x'),
('z', 'z')])
dfs(G)
请注意,在此示例中, time
未捕获in CLRS的伪代码,因为我们仅对检测周期感兴趣。还有一些样板代码,用于从边列表中构建图的邻接列表表示。
执行此脚本后,它将输出以下输出:
Cycle detected: found a back edge from x to v.
Cycle detected: found a back edge from z to z.
这些正是CLRS图22.4中示例的后边缘。
从DFS开始:只有当在DFS期间发现后端时,才会存在一个循环。这是白径定理的结果。
在我看来,用于检测有向图中循环的最容易理解的算法是图着色算法。
基本上,图着色算法以DFS方式遍历图(“深度优先搜索”,这意味着它在探索另一条路径之前先完全探索一条路径)。当找到后边缘时,它将图形标记为包含循环。
有关图形着色算法的深入说明,请阅读以下文章:http : //www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/
此外,我在JavaScript中提供了图形着色的实现https://github.com/dexcodeinc/graph_algorithm.js/blob/master/graph_algorithm.js
如果无法向节点添加“已访问”属性,请使用集合(或映射),仅将所有已访问的节点添加到集合中,除非它们已在集合中。使用唯一键或对象地址作为“键”。
这也为您提供有关循环依赖项的“根”节点的信息,当用户必须解决该问题时,该信息将派上用场。
另一个解决方案是尝试找到下一个要执行的依赖项。为此,您必须具有一些堆栈,您可以在其中记住您现在所在的位置以及下一步需要做什么。执行依赖之前,请检查该堆栈上是否已存在依赖项。如果是这样,那么您已经找到一个周期。
尽管这似乎具有O(N * M)的复杂度,但您必须记住,堆栈的深度非常有限(因此N很小),并且每个依赖项的M都会变小,您可以将其检查为“已执行”加你可以停止搜索,当你发现一个叶(所以你永远要检查每一个节点- > M将是小的,太)。
在MetaMake中,我将图形创建为列表列表,然后在执行它们时删除了每个节点,这自然减少了搜索量。实际上,我从来不需要运行独立的检查,而是在正常执行期间自动进行所有检查。
如果需要“仅测试”模式,只需添加“空运行”标志,该标志将禁用实际作业的执行。
没有一种算法可以在多项式时间内找到有向图中的所有循环。假设有向图有n个节点,并且每对节点之间都有相互连接,这意味着您有一个完整的图。因此,这n个节点的任何非空子集都表示一个周期,并且此类子集的数量为2 ^ n-1。因此,不存在多项式时间算法。因此,假设您有一个有效的(非愚蠢的)算法,该算法可以告诉您图中的有向循环数,则可以先找到强连接的组件,然后将算法应用于这些连接的组件。由于循环仅存在于组件内部,而不存在于组件之间。
我这样做的方法是进行拓扑排序,计算访问的顶点数量。如果该数目小于DAG中的顶点总数,则您有一个循环。
/mathpro/16393/finding-a-cycle-of-fixed-length我最喜欢这种解决方案,特别适合4种长度:)
此外,phys向导还说您必须执行O(V ^ 2)。我相信我们只需要O(V)/ O(V + E)。如果该图已连接,则DFS将访问所有节点。如果图具有连接的子图,则每次我们在该子图的顶点上运行DFS时,我们都将找到连接的顶点,并且在下次运行DFS时不必考虑这些顶点。因此,为每个顶点运行的可能性是不正确的。
如果DFS找到指向已访问顶点的边,则在那里存在一个循环。
如您所说,您有一组作业,需要按一定顺序执行。Topological sort
给定您所需的工作安排顺序(如果是,则解决依赖问题direct acyclic graph
)。运行dfs
并维护一个列表,如果遇到已经访问过的节点,则开始在列表的开头添加节点。然后,您在给定图中找到了一个循环。
如果图满足此属性
|e| > |v| - 1
那么该图至少包含周期。