在有向图中检测周期的最佳算法


395

检测有向图内所有周期的最有效算法是什么?

我有一个有向图,表示需要执行的作业计划,作业是一个节点,而依赖项是一个边缘。我需要检测此图中导致循环依赖性的循环的错误情况。


13
您说您想检测所有周期,但是您的用例表明足以检测是否存在任何周期。
史蒂夫·杰索普

29
这将是更好地检测所有周期,因此他们可能被固定在一个去,而不是检查,修复,检查,修复等
Peauters

2
您应该阅读Donald B. Johnson的论文“查找有向图的所有基本电路”。它只会找到基本电路,但这对于您的情况应该足够了。这是我准备使用的该算法的Java实现:github.com/1123/johnson
user152468

运行DFS,并对算法进行其他修改:标记您访问的每个节点。如果您访问已被访问的节点,那么您将获得提示。当您从路径撤退时,请取消标记访问的节点。
希舍姆·亚辛

2
@HeshamYassin,如果您访问已访问的节点,则不一定意味着存在循环。请阅读我的评论cs.stackexchange.com/questions/9676/…
Maksim Dmitriev

Answers:


193

Tarjan的强连接组件算法具有O(|E| + |V|)时间复杂性。

有关其他算法,请参阅Wikipedia上的牢固连接的组件


69
如何找到紧密连接的组件如何告诉您图中存在的循环?
彼得

4
也许有人可以确认,但是Tarjan算法不支持直接指向自己的节点的循环,例如A-> A。
塞德里克Guillemette

24
@Cedrik对,不是直接。这不是Tarjan算法的缺陷,而是用于此问题的方式。Tarjan不会直接找到循环,而是会找到牢固连接的组件。当然,任何大小大于1的SCC都意味着一个周期。非循环组件本身具有单例SCC。问题在于,自循环也将自己进入SCC。因此,您需要对自循环进行单独检查,这很简单。
mgiuca

13
(图中所有强连接的组件)!=(图中所有周期)
optimusfrenk 2015年

4
@aku:三色DFS的运行时也相同O(|E| + |V|)。使用白色(从未访问过),灰色(已访问当前节点但尚未访问所有可访问节点)和黑色(已访问所有可访问节点以及当前节点已访问)颜色编码,如果一个灰色节点找到了另一个灰色节点,则我们一个周期。[几乎是我们在Cormen的算法书中所拥有的]。想知道“ Tarjan算法”是否比这种DFS有任何好处!!
KGhatak

73

鉴于这是一份工作时间表,我怀疑您有时会进行排序它们按建议的执行顺序。

如果是这样,则拓扑排序实现无论如何都可以检测到周期。UNIX tsort当然可以。我认为,因此有可能在tsorting的同时而不是在单独的步骤中更有效地检测周期。

因此问题可能变成“我如何最有效地进行排序”,而不是“我如何最有效地检测循环”。答案可能是“使用图书馆”,但未能获得以下维基百科文章:

http://en.wikipedia.org/wiki/Topological_sorting

包含一种算法的伪代码,以及来自Tarjan的另一种算法的简短描述。两者都有O(|V| + |E|)时间复杂性。


拓扑排序可以检测周期,因为它依赖于深度优先搜索算法,但是您需要额外的簿记才能实际检测周期。请参阅Kurt Peek的正确答案。
Luke Hutchison

33

最简单的方法是对图形进行深度优先遍历(DFT)

如果图具有n顶点,则这是O(n)时间复杂度算法。由于您可能必须从每个顶点开始进行DFT,因此总复杂度变为O(n^2)

您必须维护一个堆栈,堆栈包含当前深度优先遍历的所有顶点,并且其第一个元素为根节点。如果您在DFT期间遇到了已经在堆栈中的元素,那么您就有一个循环。


21
对于“规则”图,这是正确的,但对于有图,则是错误的。例如,考虑具有四个节点的“钻石依赖图”:边指向B和C的A,每个边都指向D的边。您从A对该图的DFT遍历将错误地得出结论,“循环”为实际上是一个循环-尽管有一个循环,但它不是一个循环,因为不能通过遵循箭头来遍历。
彼得

9
@peter您能解释一下A的DFT如何错误地得出一个周期吗?
迪帕克'09

10
@Deepak-实际上,我误解了“ phys向导”的答案:他在“堆栈中”中写道:我以为“已经被发现”。在执行DFT期间检查“堆栈中”重复项确实足够(用于检测定向循环)。每个人一个赞。
彼得

2
为什么要说时间复杂度是O(n)在建议检查堆栈以查看它是否已包含访问节点时?扫描堆栈会增加O(n)运行时间,因为它必须扫描每个新节点上的堆栈。O(n)如果您标记访问的节点,则可以实现
James Wierzba

正如彼得所说,这对于有向图是不完整的。请参阅Kurt Peek的正确答案。
Luke Hutchison

31

根据Cormen等人的引理22.11,算法简介(CLRS):

当且仅当深度优先搜索G不产生后边缘时,有向图G是非循环的。

在几个答案中已经提到了这一点。在这里,我还将基于CLRS的第22章提供一个代码示例。示例图如下所示。

在此处输入图片说明

CLRS的深度优先搜索伪代码为:

在此处输入图片说明

在CLRS图22.4中的示例中,该图由两个DFS树组成:一个由节点uvxy组成,另一个由节点wz组成。每棵树都包含一个后边缘:一个从xv,另一个从zz(自环)。

关键的实现是,当在DFS-VISIT函数中遍历的邻居vu,在节点上遇到节点时会遇到后边缘。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中示例的后边缘。


4
这是这里唯一相关的,可接受的和有效的答案,值得更多的批评。
plasmacel

29

从DFS开始:只有当在DFS期间发现后端时,才会存在一个循环。这是白径定理的结果。


3
是的,我也这么认为,但这还不够,我将自己的方式张贴
jonaprieto

真正。阿杰·加格(Ajay Garg)只是在讲述如何找到“循环”,这是该问题的部分答案。您的链接讨论了根据所提出的问题查找所有周期,但是再次看起来,它使用的方法与Ajay Garg相同,但也使用了所有可能的dfs-tree。
Manohar Reddy Poreddy

对于有向图,这是不完整的。请参阅Kurt Peek的正确答案。
Luke Hutchison

26

在我看来,用于检测有向图中循环的最容易理解的算法是图着色算法。

基本上,图着色算法以DFS方式遍历图(“深度优先搜索”,这意味着它在探索另一条路径之前先完全探索一条路径)。当找到后边缘时,它将图形标记为包含循环。

有关图形着色算法的深入说明,请阅读以下文章:http : //www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/

此外,我在JavaScript中提供了图形着色的实现https://github.com/dexcodeinc/graph_algorithm.js/blob/master/graph_algorithm.js


8

如果无法向节点添加“已访问”属性,请使用集合(或映射),仅将所有已访问的节点添加到集合中,除非它们已在集合中。使用唯一键或对象地址作为“键”。

这也为您提供有关循环依赖项的“根”节点的信息,当用户必须解决该问题时,该信息将派上用场。

另一个解决方案是尝试找到下一个要执行的依赖项。为此,您必须具有一些堆栈,您可以在其中记住您现在所在的位置以及下一步需要做什么。执行依赖之前,请检查该堆栈上是否已存在依赖项。如果是这样,那么您已经找到一个周期。

尽管这似乎具有O(N * M)的复杂度,但您必须记住,堆栈的深度非常有限(因此N很小),并且每个依赖项的M都会变小,您可以将其检查为“已执行”加你可以停止搜索,当你发现一个叶(所以你永远要检查每一个节点- > M将是小的,太)。

在MetaMake中,我将图形创建为列表列表,然后在执行它们时删除了每个节点,这自然减少了搜索量。实际上,我从来不需要运行独立的检查,而是在正常执行期间自动进行所有检查。

如果需要“仅测试”模式,只需添加“空运行”标志,该标志将禁用实际作业的执行。


7

没有一种算法可以在多项式时间内找到有向图中的所有循环。假设有向图有n个节点,并且每对节点之间都有相互连接,这意味着您有一个完整的图。因此,这n个节点的任何非空子集都表示一个周期,并且此类子集的数量为2 ^ n-1。因此,不存在多项式时间算法。因此,假设您有一个有效的(非愚蠢的)算法,该算法可以告诉您图中的有向循环数,则可以先找到强连接的组件,然后将算法应用于这些连接的组件。由于循环仅存在于组件内部,而不存在于组件之间。


1
如果将节点数作为输入的大小,则为true。您还可以根据边数甚至循环数或这些措施的组合来描述运行时复杂性。唐纳德·B·约翰逊(Donald B. Johnson)的算法“查找有向图的所有基本电路”具有由O((n + e)(c + 1))给出的多项式运行时间,其中n是节点数,e是边数c图的基本电路数。这是我对该算法的Java实现:github.com/1123/johnson
user152468

4

我已经在sml(命令式编程)中实现了这个问题。这是大纲。查找入度或出度为0的所有节点。这样的节点不能成为循环的一部分(因此请删除它们)。接下来,从此类节点中删除所有传入或传出边缘。将这个过程递归地应用于结果图。如果最后没有剩下任何节点或边,则该图没有任何循环,否则就没有。


2

我这样做的方法是进行拓扑排序,计算访问的顶点数量。如果该数目小于DAG中的顶点总数,则您有一个循环。


4
那没有意义。如果图具有周期,则不存在拓扑排序,这意味着任何正确的拓扑排序算法都将中止。
sleske

4
来自Wikipedia:许多拓扑排序算法也将检测循环,因为这是存在拓扑顺序的障碍。
Oleg Mikheev

1
@OlegMikheev是的,但史蒂夫说“如果该数目小于DAG中顶点的总数,则您有一个循环”,这没有任何意义。
nbro

@nbro我敢打赌,它们的意思是拓扑排序算法的一种变体,当不存在拓扑排序时中止(然后他们不访问所有顶点)。
maaartinus

如果对具有周期的图进行拓扑排序,则最终将得到不良边最少的订单(订单号>邻居的订单号)。但是,在您必须对其进行排序之后,很容易检测到那些不良边缘,从而导致检测到带有循环的图形
UGP

2

/mathpro/16393/finding-a-cycle-of-fixed-length我最喜欢这种解决方案,特别适合4种长度:)

此外,phys向导还说您必须执行O(V ^ 2)。我相信我们只需要O(V)/ O(V + E)。如果该图已连接,则DFS将访问所有节点。如果图具有连接的子图,则每次我们在该子图的顶点上运行DFS时,我们都将找到连接的顶点,并且在下次运行DFS时不必考虑这些顶点。因此,为每个顶点运行的可能性是不正确的。


1

如果DFS找到指向已访问顶点的边,则在那里存在一个循环。


1
失败于1,2,3:1,2; 1,3; 2,3;
吵闹的猫

4
@JakeGreene看这里:i.imgur.com/tEkM5xy.png简单到足以理解。假设您从0开始。然后转到节点1,从那里不再有路径,递归返回。现在,您访问节点2,该节点的顶点1的边已经被访问过。在您看来,您将有一个周期-然后您没有一个真正的
吵闹的猫

3
@kittyPL该图不包含循环。来自维基百科:“有向图中的有向环是在相同顶点处开始和结束的一系列顶点,这样,对于该循环的每两个连续顶点,都存在从较早顶点指向后一个顶点的边”必须能够遵循从V到有向循环再回到V的路径。mafonya的解决方案适用于给定的问题
Jake Greene

2
@JakeGreene当然不是。使用您的算法并从1开始,无论如何都会检测到一个循环...这个算法很糟糕...通常,只要遇到访问的顶点,就足以向后走。
吵闹的猫

6
@kittyPL DFS确实可以检测来自给定起始节点的周期。但是,在执行DFS时,必须为访问的节点着色,以区分交叉边缘和后边缘。第一次访问顶点时,它会变成灰色,然后在访问完所有边缘后将其变成黑色。如果在执行DFS时遇到灰色顶点,则该顶点是祖先(即:您有一个循环)。如果顶点是黑色的,那么它只是一个交叉边缘。
Kyrra 2014年

0

如您所说,您有一组作业,需要按一定顺序执行。Topological sort给定您所需的工作安排顺序(如果是,则解决依赖问题direct acyclic graph)。运行dfs并维护一个列表,如果遇到已经访问过的节点,则开始在列表的开头添加节点。然后,您在给定图中找到了一个循环。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.