用于检测有向图中的循环的最佳算法

Best algorithm for detecting cycles in a directed graph

在有向图中检测所有循环的最有效算法是什么?

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


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

有关其他算法,请参见维基百科上的强连接组件。


考虑到这是一个作业计划表,我怀疑在某个时刻,您将把它们排序成一个建议的执行顺序。

如果是这样,那么拓扑排序实现在任何情况下都可以检测循环。unix tsort当然可以。我认为它很可能是更有效的检测周期在同一时间的排序,而不是在一个单独的步骤。

因此,问题可能变成"我如何最有效地进行排序",而不是"我如何最有效地检测循环"。答案可能是"使用一个图书馆",但如果不能做到这一点,请参阅下面的维基百科文章:

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

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


从一个DFS开始:当且仅当在DFS期间发现一个后缘时,才存在一个循环。这是白道理论的结果。


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

如果图有n顶点,这是一个O(n)时间复杂度算法。因为您可能需要从每个顶点开始进行DFT,所以总的复杂性变成了O(n^2)

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


在我看来,最容易理解的有向图周期检测算法是图着色算法。

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

有关图形着色算法的深入解释,请阅读本文:http://www.geeksforgeks.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个这样的子集。因此不存在多项式时间算法。所以假设你有一个有效的(非愚蠢的)算法,它可以告诉你一个图中有向循环的数量,你可以先找到强连接组件,然后把你的算法应用到这些连接组件上。因为周期只存在于组件内部,而不存在于组件之间。


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

A directed graph G is acyclic if and only if a depth-first search of G yields no back edges.

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

enter image description here

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

氧化镁

在CLRS图22.4中的示例中,图由两个DFS树组成:一个由节点U、V、X和Y组成,另一个由节点W和Z组成。每个树都包含一个后缘:一个从X到V,另一个从Z到Z(自循环)。

关键的实现是,在DFS-VISIT函数中,当迭代u的相邻v时,遇到一个具有GRAY颜色的节点时,会遇到一个后缘。

下面的python代码是CLRS伪代码的一种改编,添加了一个if子句,用于检测循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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 discovered and 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)

注意,在这个例子中,没有捕获CLRS伪代码中的time,因为我们只对检测周期感兴趣。还有一些样板代码用于从边列表构建图形的邻接列表表示。

执行此脚本时,将打印以下输出:

1
2
Cycle detected: found a back edge from x to v.
Cycle detected: found a back edge from z to z.

这些正是CLRS图22.4中示例中的后边缘。


如果DFS找到一个指向已访问顶点的边,则在该边上有一个循环。


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


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


https://mathoverflow.net/questions/16393/finding-a-cycle-of-fixed-length我最喜欢这个解决方案,特别是针对4个长度:)

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


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


如果图满足此属性

1
|e| > |v| - 1

然后图表至少包含on循环。