如何检查有向图是否为非循环图?


82

如何检查有向图是否为非循环图?以及如何调用算法?我将不胜感激。


另一种支持某种方式来“修复” SO错误答案的案例。
Sparr

2
因此,嗯,我最感兴趣的是找到它所需的时间。所以,我只需要抽象算法。
nes1983

您必须遍历所有边并检查所有顶点,以便下界为O(| V | + | E |)。DFS和BFS的复杂度相同,但是如果您有递归,则DFS更容易编码,因为它可以为您管理堆栈...
ShuggyCoUk 2009年

DFS的复杂性一样。考虑具有节点{1 .. N}且边线形式为{(a,b)| a <b}。该图是非循环的,但DFS为O(n!)
FryGuy

1
DFS从不为O(n!)。它访问每个节点一次,每个边缘最多访问两次。所以O(| V | + | E |)或O(n)。
杰伊·康罗德

Answers:


95

我会尝试按拓扑对图进行排序,如果不能,则它具有循环。


2
怎么没有票?它在节点+边缘上呈线性,远远优于O(n ^ 2)解决方案!
罗伦·佩希特尔

5
在许多情况下,DFS(请参阅J.Conrod的答案)可能会更容易,尤其是在无论如何都需要执行DFS的情况下。但这当然取决于上下文。
sleske

1
拓扑顺序将处于无限循环中,但不会告诉我们循环发生的位置...
Baradwaj Aryasomayajula 2015年

35

做一个简单的深度优先搜索是不是足够好找到一个循环。可以在不存在周期的情况下在DFS中多次访问节点。根据开始的位置,您可能也不会访问整个图形。

您可以按以下方式检查图的连接组件中的周期。查找仅具有向外边缘的节点。如果没有这样的节点,则存在一个周期。在该节点上启动DFS。遍历每个边时,请检查边是否指向堆栈中已存在的节点。这表明存在循环。如果找不到这样的边缘,则该连接的组件中没有循环。

正如Rutger Prins指出的那样,如果图形未连接,则需要在每个连接的组件上重复搜索。

作为参考,Tarjan的强连接组件算法密切相关。它还将帮助您找到周期,而不仅仅是报告周期是否存在。


2
顺便说一句:出于明显的原因,“指向存储栈中已经存在的节点”的边缘在文献中通常称为“后边缘”。是的,这可能比通过拓扑对图形进行排序更简单,尤其是如果您仍然需要执行DFS时。
sleske

为了使图成为非循环图,可以说每个连接的组件都必须包含一个仅具有输出边的节点。您是否可以推荐一种算法,以找到有向图的连接组件(而不是“强连接”组件),以供您的主要算法使用?
kostmo'9

@kostmo,如果图形具有多个连接的组件,则您将不会访问第一个DFS中的所有节点。跟踪您访问过的节点,并对未访问的节点重复该算法,直到全部到达为止。无论如何,这基本上就是连接组件算法的工作方式。
杰·康罗德

6
尽管此答案的意图是正确的,但如果使用基于堆栈的DFS实现,则答案会令人困惑:用于实现DFS的堆栈将不包含要测试的正确元素。有必要向用于跟踪祖先节点集的算法中添加一个额外的堆栈。
西奥多·默多克

关于您的答案,我有多个问题。我张贴他们在这里:stackoverflow.com/questions/37582599/...
阿里


9

解决方案1卡恩算法检查周期。主要思想:维护一个队列,将度数为零的节点添加到队列中。然后一个接一个地剥离节点,直到队列为空。检查是否存在任何节点的边缘。

解决方案2Tarjan算法检查强连接的组件。

解决方案3DFS。使用整数数组标记节点的当前状态:即0-表示此节点之前未访问过。-1-表示该节点已被访问,其子节点已被访问。1-表示已访问此节点,并且已完成。因此,如果在执行DFS时节点的状态为-1,则意味着必须存在一个循环。


1

ShuggyCoUk提供的解决方案是不完整的,因为它可能不会检查所有节点。


def isDAG(nodes V):
    while there is an unvisited node v in V:
        bool cycleFound = dfs(v)
        if cyclefound:
            return false
    return true

时间复杂度为O(n + m)或O(n ^ 2)


我的确实是不正确的-尽管我删除了它,所以您的位置似乎有点脱离上下文了
ShuggyCoUk 2009年

3
O(n + m)<= O(n + n)= O(2n),O(2n)!= O(n ^ 2)
Artru 2011年

@Artru使用邻接矩阵时为O(n ^ 2),使用邻接表表示图时为O(n + m)。
0x450

嗯...m = O(n^2)因为完整的图形具有精确的m=n^2边缘。就是这样O(n+m) = O(n + n^2) = O(n^2)
Alex Reinking

1

我知道这是一个老话题,但是对于未来的搜索者来说,这是我创建的C#实现(没有声称它是最有效的!)。它旨在使用一个简单的整数来标识每个节点。您可以根据需要装饰,只要您的节点对象哈希值正确且相等即可。

对于非常深的图,这可能会产生很高的开销,因为它会在每个深度节点上创建一个哈希集(它们会在宽度上被破坏)。

输入要从中搜索的节点,然后输入到该节点的路径。

  • 对于具有单个根节点的图,您发送该节点和一个空哈希集
  • 对于具有多个根节点的图,您可以将其包装在这些节点的foreach中,并为每次迭代传递一个新的空哈希集
  • 在检查任何给定节点以下的周期时,只需将该节点与一个空哈希集一起传递

    private bool FindCycle(int node, HashSet<int> path)
    {
    
        if (path.Contains(node))
            return true;
    
        var extendedPath = new HashSet<int>(path) {node};
    
        foreach (var child in GetChildren(node))
        {
            if (FindCycle(child, extendedPath))
                return true;
        }
    
        return false;
    }
    

1

在执行DFS时不应有任何后沿。在执行DFS时请跟踪已访问的节点,如果遇到当前节点与现有节点之间的边缘,则图具有循环。


1

这是一个快速代码,用于查找图形是否具有循环:

func isCyclic(G : Dictionary<Int,Array<Int>>,root : Int , var visited : Array<Bool>,var breadCrumb : Array<Bool>)-> Bool
{

    if(breadCrumb[root] == true)
    {
        return true;
    }

    if(visited[root] == true)
    {
        return false;
    }

    visited[root] = true;

    breadCrumb[root] = true;

    if(G[root] != nil)
    {
        for child : Int in G[root]!
        {
            if(isCyclic(G,root : child,visited : visited,breadCrumb : breadCrumb))
            {
                return true;
            }
        }
    }

    breadCrumb[root] = false;
    return false;
}


let G = [0:[1,2,3],1:[4,5,6],2:[3,7,6],3:[5,7,8],5:[2]];

var visited = [false,false,false,false,false,false,false,false,false];
var breadCrumb = [false,false,false,false,false,false,false,false,false];




var isthereCycles = isCyclic(G,root : 0, visited : visited, breadCrumb : breadCrumb)

这个想法是这样的:一个普通的dfs算法,具有一个数组来跟踪访问的节点,还有一个额外的数组,该数组用作通向当前节点的节点的标记,因此,无论何时我们为一个节点执行dfs我们将其在标记数组中的对应项设置为true,以便在遇到一个已经访问过的节点时,检查其在标记数组中的对应项是否为true,如果为true,则检查其自身的节点之一(因此循环),诀窍是只要节点的dfs返回,我们便将其对应的标记设置回false,这样,如果我们从另一条路线再次访问它,就不会上当。


0

这是我的剥离叶节点算法的红宝石实现。

def detect_cycles(initial_graph, number_of_iterations=-1)
    # If we keep peeling off leaf nodes, one of two things will happen
    # A) We will eventually peel off all nodes: The graph is acyclic.
    # B) We will get to a point where there is no leaf, yet the graph is not empty: The graph is cyclic.
    graph = initial_graph
    iteration = 0
    loop do
        iteration += 1
        if number_of_iterations > 0 && iteration > number_of_iterations
            raise "prevented infinite loop"
        end

        if graph.nodes.empty?
            #puts "the graph is without cycles"
            return false
        end

        leaf_nodes = graph.nodes.select { |node| node.leaving_edges.empty? }

        if leaf_nodes.empty?
            #puts "the graph contain cycles"
            return true
        end

        nodes2 = graph.nodes.reject { |node| leaf_nodes.member?(node) }
        edges2 = graph.edges.reject { |edge| leaf_nodes.member?(edge.destination) }
        graph = Graph.new(nodes2, edges2)
    end
    raise "should not happen"
end

0

刚刚在Google访谈中遇到了这个问题。

拓扑排序

您可以尝试按拓扑排序,即O(V + E),其中V是顶点数,E是边数。有向图在且仅当可以做到时才是非循环的。

递归叶去除

递归地删除叶节点,直到没有剩下的节点为止;如果还剩下一个以上的节点,那么您就有一个循环。除非我弄错了,否则这是O(V ^ 2 + VE)。

DFS样式〜O(n + m)

但是,最有效的DFS式算法(最坏情况为O(V + E))是:

function isAcyclic (root) {
    const previous = new Set();

    function DFS (node) {
        previous.add(node);

        let isAcyclic = true;
        for (let child of children) {
            if (previous.has(node) || DFS(child)) {
                isAcyclic = false;
                break;
            }
        }

        previous.delete(node);

        return isAcyclic;
    }

    return DFS(root);
}

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.