简体   繁体   English

如何用改进的DFS算法遍历循环有向图

[英]How to traverse cyclic directed graphs with modified DFS algorithm

OVERVIEW 概述

I'm trying to figure out how to traverse directed cyclic graphs using some sort of DFS iterative algorithm. 我试图找出如何使用某种DFS迭代算法遍历有向循环图 Here's a little mcve version of what I currently got implemented (it doesn't deal with cycles): 这是我目前实现的一个小版本(它不涉及周期):

class Node(object):

    def __init__(self, name):
        self.name = name

    def start(self):
        print '{}_start'.format(self)

    def middle(self):
        print '{}_middle'.format(self)

    def end(self):
        print '{}_end'.format(self)

    def __str__(self):
        return "{0}".format(self.name)


class NodeRepeat(Node):

    def __init__(self, name, num_repeats=1):
        super(NodeRepeat, self).__init__(name)
        self.num_repeats = num_repeats


def dfs(graph, start):
    """Traverse graph from start node using DFS with reversed childs"""

    visited = {}
    stack = [(start, "")]
    while stack:
        # To convert dfs -> bfs
        # a) rename stack to queue
        # b) pop becomes pop(0)
        node, parent = stack.pop()
        if parent is None:
            if visited[node] < 3:
                node.end()
            visited[node] = 3
        elif node not in visited:
            if visited.get(parent) == 2:
                parent.middle()
            elif visited.get(parent) == 1:
                visited[parent] = 2

            node.start()
            visited[node] = 1

            stack.append((node, None))

            # Maybe you want a different order, if it's so, don't use reversed
            childs = reversed(graph.get(node, []))
            for child in childs:
                if child not in visited:
                    stack.append((child, node))


if __name__ == "__main__":
    Sequence1 = Node('Sequence1')
    MtxPushPop1 = Node('MtxPushPop1')
    Rotate1 = Node('Rotate1')
    Repeat1 = NodeRepeat('Repeat1', num_repeats=2)

    Sequence2 = Node('Sequence2')
    MtxPushPop2 = Node('MtxPushPop2')
    Translate = Node('Translate')
    Rotate2 = Node('Rotate2')
    Rotate3 = Node('Rotate3')
    Scale = Node('Scale')
    Repeat2 = NodeRepeat('Repeat2', num_repeats=3)
    Mesh = Node('Mesh')

    cyclic_graph = {
        Sequence1: [MtxPushPop1, Rotate1],
        MtxPushPop1: [Sequence2],
        Rotate1: [Repeat1],
        Sequence2: [MtxPushPop2, Translate],
        Repeat1: [Sequence1],
        MtxPushPop2: [Rotate2],
        Translate: [Rotate3],
        Rotate2: [Scale],
        Rotate3: [Repeat2],
        Scale: [Mesh],
        Repeat2: [Sequence2]
    }

    dfs(cyclic_graph, Sequence1)

    print '-'*80

    a = Node('a')
    b = Node('b')
    dfs({
        a : [b],
        b : [a]
    }, a)

The above code is testing a couple of cases, the first would be some sort of representation of the below graph: 上面的代码测试了几个案例,第一个是下图的某种表示:

在此输入图像描述

The second one is the simplest case of one graph containing one "infinite" loop {a->b, b->a} 第二个是最简单的情况,一个图包含一个“无限”循环{a->b, b->a}

REQUIREMENTS 要求

  • There won't exist such a thing like "infinite cycles", let's say when one "infinite cycle" is found, there will be a maximum threshold (global var) to indicate when to stop looping around those "pseudo-infinite cycles" 不存在像“无限循环”这样的事情,假设当找到一个“无限循环”时,将会有一个最大阈值(全局变量)来指示何时停止在那些“伪无限循环”周围循环
  • All graph nodes are able to create cycles but there will exist a special node called Repeat where you can indicate how many iterations to loop around the cycle 所有图形节点都能够创建循环但是会存在一个名为Repeat的特殊节点,您可以在其中指示循环循环的循环次数
  • The above mcve I've posted is an iterative version of the traversal algorithm which doesn't know how to deal with cyclic graphs . 我发布的上述mcve是遍历算法的迭代版本,它不知道如何处理循环图 Ideally the solution would be also iterative but if there exists a much better recursive solution, that'd be great 理想情况下,解决方案也是迭代的,但如果存在更好的递归解决方案,那就太好了
  • The data structure we're talking about here shouldn't be called "directed acyclic graphs" really because in this case, each node has its children ordered , and in graphs node connections have no order. 我们在这里讨论的数据结构不应该被称为“有向非循环图”,因为在这种情况下, 每个节点都有其子节点 ,而在图中节点连接没有顺序。
  • Everything can be connected to anything in the editor. 一切都可以连接到编辑器中的任何内容。 You'll be able to execute any block combination and the only limitation is the execution counter, which will overflow if you made neverending loop or too many iterations. 您将能够执行任何块组合,唯一的限制是执行计数器,如果您进行了无限循环或过多的迭代,它将会溢出。
  • The algorithm will preserve start/middle/after node's method execution similarly than the above snippet 算法将保留开始/中间/后节点的方法执行,类似于上面的代码片段

QUESTION

Could anyone provide some sort of solution which knows how to traverse infinite/finite cycles? 任何人都可以提供某种知道如何遍历无限/有限循环的解决方案吗?

REFERENCES 参考

If question is not clear yet at this point, you can read this more about this problem on this article , the whole idea will be using the traversal algorithm to implement a similar tool like the shown in that article. 如果问题还不清楚在这一点上,你可以阅读更多有关这对这个问题的文章 ,整个构思将使用遍历算法来实现这样的文章中所示的类似的工具。

Here's a screenshot showing up the whole power of this type of data structure I want to figure out how to traverse&run: 这是一个屏幕截图,显示了这种数据结构的全部功能我想弄清楚如何遍历和运行:

在此输入图像描述

Before I start, Run the code on CodeSkulptor! 在开始之前, 在CodeSkulptor上运行代码! I also hope that the comments elaborate what I have done enough. 我也希望这些评论能够详细说明我所做的工作。 If you need more explanation, look at my explanation of the recursive approach below the code. 如果您需要更多解释,请查看我对代码下面的递归方法的解释。

# If you don't want global variables, remove the indentation procedures
indent = -1

MAX_THRESHOLD = 10
INF = 1 << 63

def whitespace():
    global indent
    return '|  ' * (indent)

class Node:
    def __init__(self, name, num_repeats=INF):
        self.name = name
        self.num_repeats = num_repeats

    def start(self):
        global indent
        if self.name.find('Sequence') != -1:
            print whitespace()
            indent += 1
        print whitespace() + '%s_start' % self.name

    def middle(self):
        print whitespace() + '%s_middle' % self.name

    def end(self):
        global indent
        print whitespace() + '%s_end' % self.name
        if self.name.find('Sequence') != -1:
            indent -= 1
            print whitespace()

def dfs(graph, start):
    visits = {}
    frontier = [] # The stack that keeps track of nodes to visit

    # Whenever we "visit" a node, increase its visit count
    frontier.append((start, start.num_repeats))
    visits[start] = visits.get(start, 0) + 1

    while frontier:
        # parent_repeat_count usually contains vertex.repeat_count
        # But, it may contain a higher value if a repeat node is its ancestor
        vertex, parent_repeat_count = frontier.pop()

        # Special case which signifies the end
        if parent_repeat_count == -1:
            vertex.end()
            # We're done with this vertex, clear visits so that 
            # if any other node calls us, we're still able to be called
            visits[vertex] = 0
            continue

        # Special case which signifies the middle
        if parent_repeat_count == -2:
            vertex.middle()
            continue  

        # Send the start message
        vertex.start()

        # Add the node's end state to the stack first
        # So that it is executed last
        frontier.append((vertex, -1))

        # No more children, continue
        # Because of the above line, the end method will
        # still be executed
        if vertex not in graph:
            continue

        ## Uncomment the following line if you want to go left to right neighbor
        #### graph[vertex].reverse()

        for i, neighbor in enumerate(graph[vertex]):
            # The repeat count should propagate amongst neighbors
            # That is if the parent had a higher repeat count, use that instead
            repeat_count = max(1, parent_repeat_count)
            if neighbor.num_repeats != INF:
                repeat_count = neighbor.num_repeats

            # We've gone through at least one neighbor node
            # Append this vertex's middle state to the stack
            if i >= 1:
                frontier.append((vertex, -2))

            # If we've not visited the neighbor more times than we have to, visit it
            if visits.get(neighbor, 0) < MAX_THRESHOLD and visits.get(neighbor, 0) < repeat_count:
                frontier.append((neighbor, repeat_count))
                visits[neighbor] = visits.get(neighbor, 0) + 1

def dfs_rec(graph, node, parent_repeat_count=INF, visits={}):
    visits[node] = visits.get(node, 0) + 1

    node.start()

    if node not in graph:
        node.end()
        return

    for i, neighbor in enumerate(graph[node][::-1]):
        repeat_count = max(1, parent_repeat_count)
        if neighbor.num_repeats != INF:
            repeat_count = neighbor.num_repeats

        if i >= 1:
            node.middle()

        if visits.get(neighbor, 0) < MAX_THRESHOLD and visits.get(neighbor, 0) < repeat_count:
            dfs_rec(graph, neighbor, repeat_count, visits)

    node.end()  
    visits[node] = 0

Sequence1 = Node('Sequence1')
MtxPushPop1 = Node('MtxPushPop1')
Rotate1 = Node('Rotate1')
Repeat1 = Node('Repeat1', 2)

Sequence2 = Node('Sequence2')
MtxPushPop2 = Node('MtxPushPop2')
Translate = Node('Translate')
Rotate2 = Node('Rotate2')
Rotate3 = Node('Rotate3')
Scale = Node('Scale')
Repeat2 = Node('Repeat2', 3)
Mesh = Node('Mesh')

cyclic_graph = {
        Sequence1: [MtxPushPop1, Rotate1],
        MtxPushPop1: [Sequence2],
        Rotate1: [Repeat1],
        Sequence2: [MtxPushPop2, Translate],
        Repeat1: [Sequence1],
        MtxPushPop2: [Rotate2],
        Translate: [Rotate3],
        Rotate2: [Scale],
        Rotate3: [Repeat2],
        Scale: [Mesh],
        Repeat2: [Sequence2]
    }

dfs(cyclic_graph, Sequence1)

print '-'*40

dfs_rec(cyclic_graph, Sequence1)

print '-'*40

dfs({Sequence1: [Translate], Translate: [Sequence1]}, Sequence1)

print '-'*40

dfs_rec({Sequence1: [Translate], Translate: [Sequence1]}, Sequence1)

The input and (well formatted and indented) output can be found here . 输入和(格式良好和缩进)输出可以在这里找到。 If you want to see how I formatted the output, please refer to the code, which can also be found on CodeSkulptor . 如果您想查看我如何格式化输出,请参阅代码,该代码也可以在CodeSkulptor上找到


Right, on to the explanation. 对,解释。 The easier to understand but much more inefficient recursive solution, which I'll use to help explain, follows: 更容易理解但更低效的递归解决方案,我将用它来帮助解释,如下:

def dfs_rec(graph, node, parent_repeat_count=INF, visits={}):
    visits[node] = visits.get(node, 0) + 1

    node.start()

    if node not in graph:
        node.end()
        return

    for i, neighbor in enumerate(graph[node][::-1]):
        repeat_count = max(1, parent_repeat_count)
        if neighbor.num_repeats != INF:
            repeat_count = neighbor.num_repeats

        if i >= 1:
            node.middle()

        if visits.get(neighbor, 0) < MAX_THRESHOLD and visits.get(neighbor, 0) < repeat_count:
            dfs_rec(graph, neighbor, repeat_count, visits)

    node.end()  
    visits[node] = 0
  1. The first thing we do is visit the node. 我们要做的第一件事是访问节点。 We do this by incrementing the number of visits of the node in the dictionary. 我们通过增加字典中节点的访问次数来完成此操作。
  2. We then raise the start event of the node. 然后我们引发节点的start事件。
  3. We do a simple check to see if the node is a childless (leaf) node or not. 我们做一个简单的检查,看看节点是否是无子(叶)节点。 If it is, we raise the end event and return. 如果是,我们提出end事件并返回。
  4. Now that we've established that the node has neighbors, we iterate through each neighbor. 现在我们已经确定节点有邻居,我们遍历每个邻居。 Side Note: I reverse the neighbor list (by using graph[node][::-1] ) in the recursive version to maintain the same order (right to left) of traversal of neighbors as in the iterative version. 注意:我在递归版本中反转邻居列表(通过使用graph[node][::-1] )来维持邻居遍历的相同顺序(从右到左),如在迭代版本中那样。
    1. For each neighbor, we first calculate the repeat count. 对于每个邻居,我们首先计算重复计数。 The repeat count propagates (is inherited) through from the ancestor nodes, so the inherited repeat count is used unless the neighbor contains a repeat count value. 重复计数从祖先节点传播(继承),因此除非 邻居包含重复计数值, 否则将使用继承的重复计数。
    2. We raise the middle event of the current node ( not the neighbor) if the second (or greater) neighbor is being processed. 如果正在处理第二个(或更大的)邻居,我们会引发当前节点( 不是邻居)的middle事件。
    3. If the neighbor can be visited, the neighbor is visited. 如果可以访问邻居,则访问邻居。 The visitability check is done by checking whether the neighbor has been visited less than a) MAX_THRESHOLD times (for pseudo-infinite cycles) and b) the above calculated repeat count times. 通过检查邻居是否被访问小于a) MAX_THRESHOLD次(对于伪无限循环)和b)上述计算的重复计数次数来完成可访问性检查。
  5. We're now done with this node; 我们现在已经完成了这个节点; raise the end event and clear its visits in the hashtable. 提升end事件并清除其在哈希表中的访问。 This is done so that if some other node calls it again, it does not fail the visitability check and/or execute for less than the required number of times. 这样做是为了如果某个其他节点再次调用它,则它不会使可访问性检查失败和/或执行少于所需的次数。

As per comment66244567 - reducing the graph to a tree by ignoring links to visited nodes and performing a breadth-first search, as this would produce a more natural-looking (and likely more balanced) tree: 根据评论66244567 - 通过忽略到访问节点的链接并执行广度优先搜索将图形缩减为树,因为这将生成更自然(并且可能更平衡)的树:

def traverse(graph,node,process):
    seen={node}
    current_level=[node]
    while current_level:
        next_level=[]
        for node in current_level:
            process(node)
            for child in (link for link in graph.get(node,[]) if link not in seen):
                next_level.append(child)
                seen.add(child)
        current_level=next_level

With your graph and def process(node): print node , this produces: 使用图形和def process(node): print node ,这会产生:

In [24]: traverse(cyclic_graph,Sequence1,process)
Sequence1
MtxPushPop1
Rotate1
Sequence2
Repeat1
MtxPushPop2
Translate
Rotate2
Rotate3
Scale
Repeat2
Mesh

The other BFS algorithm, iterative deepening DFS (uses less memory at the cost of speed) isn't going to win you anything in this case: since you have to store references to visited nodes, you already consume O(n) memory. 另一个BFS算法, 迭代加深DFS (以速度为代价使用更少的内存)在这种情况下不会赢得任何东西:因为你必须存储对被访问节点的引用,你已经消耗了O(n)内存。 Neither you need to produce intermediate results (but you can anyway - eg yield something after processing a level). 你不需要产生中间结果(但无论如何你都可以 - 例如在处理一个级别后yield一些东西)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM