How to detect a cycle in a directed graph with Python?

前端 未结 3 870
你的背包
你的背包 2020-12-02 00:59

I have some input like: [(\'A\', \'B\'),(\'C\', \'D\'),(\'D\', \'C\'),(\'C\', \'D\')]. I want to look for if the existence of a cycle in a directed graph repres

相关标签:
3条回答
  • 2020-12-02 01:25

    My own implementation (non-recursive so without cycle length limit):

    from collections import defaultdict
    
    
    def has_cycle(graph):
        try:
            next(_iter_cycles(graph))
        except StopIteration:
            return False
        return True
    
    
    def _iter_cycles(edges):
        """Iterate over simple cycles in the directed graph."""
        if isinstance(edges, dict):
            graph = edges
        else:
            graph = defaultdict(set)
            for x, y in edges:
                graph[x].add(y)
        SEP = object()
        checked_nodes = set()  # already checked nodes
        for start_node in graph:
            if start_node in checked_nodes:
                continue
            nodes_left = [start_node]
            path = []         # current path from start_node
            node_idx = {}     # {node: path.index(node)}
            while nodes_left:
                node = nodes_left.pop()
                if node is SEP:
                    checked_node = path.pop()
                    del node_idx[checked_node]
                    checked_nodes.add(checked_node)
                    continue
                if node in checked_nodes:
                    continue
                if node in node_idx:
                    cycle_path = path[node_idx[node]:]
                    cycle_path.append(node)
                    yield cycle_path
                    continue
                next_nodes = graph.get(node)
                if not next_nodes:
                    checked_nodes.add(node)
                    continue
                node_idx[node] = len(path)
                path.append(node)
                nodes_left.append(SEP)
                nodes_left.extend(next_nodes)
    
    
    assert not has_cycle({0: [1, 2], 1: [3, 4], 5: [6, 7]})
    assert has_cycle([(0, 1), (1, 0), (1, 2), (2, 1)])
    
    
    def assert_cycles(graph, expected):
        detected = sorted(_iter_cycles(graph))
        if detected != expected:
            raise Exception('expected cycles:\n{}\ndetected cycles:\n{}'.format(expected, detected))
    
    
    assert_cycles([('A', 'B'),('C', 'D'),('D', 'C'),('C', 'D')], [['C', 'D', 'C']])
    assert_cycles([('A', 'B'),('B', 'A'),('B', 'C'),('C', 'B')], [['A', 'B', 'A'], ['B', 'C', 'B']])
    
    assert_cycles({1: [2, 3], 2: [3, 4]}, [])
    assert_cycles([(1, 2), (1, 3), (2, 3), (2, 4)], [])
    
    assert_cycles({1: [2, 4], 2: [3, 4], 3: [1]}, [[1, 2, 3, 1]])
    assert_cycles([(1, 2), (1, 4), (2, 3), (2, 4), (3, 1)], [[1, 2, 3, 1]])
    
    assert_cycles({0: [1, 2], 2: [3], 3: [4], 4: [2]}, [[2, 3, 4, 2]])
    assert_cycles([(0, 1), (0, 2), (2, 3), (3, 4), (4, 2)], [[2, 3, 4, 2]])
    
    assert_cycles({1: [2], 3: [4], 4: [5], 5: [3]}, [[3, 4, 5, 3]])
    assert_cycles([(1, 2), (3, 4), (4, 5), (5, 3)], [[3, 4, 5, 3]])
    
    assert_cycles({0: [], 1: []}, [])
    assert_cycles([], [])
    
    assert_cycles({0: [1, 2], 1: [3, 4], 5: [6, 7]}, [])
    assert_cycles([(0, 1), (0, 2), (1, 3), (1, 4), (5, 6), (5, 7)], [])
    
    assert_cycles({0: [1], 1: [0, 2], 2: [1]}, [[0, 1, 0], [1, 2, 1]])
    assert_cycles([(0, 1), (1, 0), (1, 2), (2, 1)], [[0, 1, 0], [1, 2, 1]])
    

    EDIT:

    I found that while has_cycle seems to be correct, the _iter_cycles does not iterate over all cycles!

    Example in which _iter_cycles does not find all cycles:

    assert_cycles([
            (0, 1), (1, 2), (2, 0),  # Cycle 0-1-2
            (0, 2), (2, 0),          # Cycle 0-2
            (0, 1), (1, 4), (4, 0),  # Cycle 0-1-4
        ],
        [
            [0, 1, 2, 0],  # Not found (in Python 3.7)!
            [0, 1, 4, 0],
            [0, 2, 0],
        ]
    )
    
    0 讨论(0)
  • 2020-12-02 01:28

    Using the networkx library, we can use the simple_cycles function to find all simple cycles of a directed Graph.

    Example Code:

    import networkx as nx
    
    edges = [('A', 'B'),('C', 'D'),('D', 'C'),('C', 'D')]
    
    G = nx.DiGraph(edges)
    
    for cycle in nx.simple_cycles(G):
        print(cycle)
    
    G = nx.DiGraph()
    
    G.add_edge('A', 'B')
    G.add_edge('B', 'C')
    G.add_edge('C', 'A')
    
    for cycle in nx.simple_cycles(G):
        print(cycle)
    

    Output:

    ['D', 'C']
    ['B', 'C', 'A']
    
    0 讨论(0)
  • 2020-12-02 01:41

    The issue is the example given at [1]: https://www.geeksforgeeks.org/detect-cycle-in-a-graph/ works for integers only because they use the range() function to create a list of nodes,see the line

    for node in range(self.V):
    

    That makes the assumption that not only will all the nodes be integers but also that they will be a contiguous set i.e. [0,1,2,3] is okay but [0,3,10] is not.

    You can fix the example if you like to work with any nodes by swapping the line given above with

    for node in self.graph.keys():
    

    which will loop through all the nodes instead of a range of numbers :)

    0 讨论(0)
提交回复
热议问题