23

概要

ある種の DFS 反復アルゴリズムを使用して、有向循環グラフをトラバースする方法を理解しようとしています。これは、私が現在実装しているものの小さな mcve バージョンです (サイクルは扱っていません)。

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)

上記のコードはいくつかのケースをテストしています。最初のケースは、以下のグラフのある種の表現になります。

ここに画像の説明を入力

2 つ目は、1 つの「無限」ループを含む 1 つのグラフの最も単純なケースです。{a->b, b->a}

要件

  • 「無限サイクル」のようなものは存在しません。たとえば、「無限サイクル」が1つ見つかった場合、それらの「疑似無限サイクル」のループをいつ停止するかを示す最大しきい値(グローバル変数)があります。
  • Repeatすべてのグラフ ノードはサイクルを作成できますが、サイクルをループする反復回数を指定できる特別なノードが存在します。
  • 私が投稿した上記の mcve は、巡回グラフの処理方法がわからないトラバーサル アルゴリズムの反復バージョンです。理想的には、ソリューションも反復的ですが、はるかに優れた再帰的ソリューションが存在する場合、それは素晴らしいことです
  • ここで話しているデータ構造は、実際には「有向非巡回グラフ」と呼ばれるべきではありません。この場合、各ノードには順序付けられた子があり、グラフではノード接続に順序がないためです。
  • すべてがエディター内の何にでも接続できます。任意のブロックの組み合わせを実行できますが、唯一の制限は実行カウンターです。これは、無限ループを作成したり、反復が多すぎたりするとオーバーフローします。
  • アルゴリズムは、上記のスニペットと同様に、ノードのメソッド実行の開始/中間/後を保持します。

質問

無限/有限サイクルを横断する方法を知っている何らかのソリューションを提供できる人はいますか?

参考文献

この時点でまだ疑問が明確でない場合は、この記事でこの問題について詳しく読むことができます。全体的なアイデアは、トラバーサル アルゴリズムを使用して、その記事に示されているような同様のツールを実装することです。

これは、トラバースして実行する方法を理解したい、このタイプのデータ構造の全機能を示すスクリーンショットです。

ここに画像の説明を入力

4

2 に答える 2

8

始める前に、CodeSkulptor でコードを実行してください。また、コメントが私が十分に行ったことを詳しく説明してくれることを願っています。さらに説明が必要な場合は、コードの下にある再帰的アプローチの説明を参照してください。

# 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)

入力と (適切にフォーマットされ、インデントされた) 出力は、ここにあります。出力をどのようにフォーマットしたかを知りたい場合は、CodeSkulptor にもあるコードを参照してください。


では、説明に入ります。理解するのは簡単ですが、はるかに非効率的な再帰的なソリューションを説明するために使用します。

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. 最初に行うことは、ノードにアクセスすることです。これを行うには、辞書内のノードの訪問回数を増やします。
  2. 次にstart、ノードのイベントを発生させます。
  3. ノードが子のない (リーフ) ノードであるかどうかを確認する簡単なチェックを行います。そうであれば、endイベントを発生させて返します。
  4. ノードに隣接ノードがあることを確認したので、各隣接ノードを反復処理します。補足:graph[node][::-1]反復バージョンと同じ順序 (右から左) を維持するために、再帰バージョンで ( を使用して) 近隣リストを逆にします。
    1. 各ネイバーについて、最初に繰り返し回数を計算します。繰り返し回数は祖先ノードから伝播 (継承) されるため、隣接ノードに繰り返し回数値が含まれていない限り、継承された繰り返し回数が使用されます。
    2. 2 番目 (またはそれ以上) の隣接ノードが処理されている場合、現在のノード (隣接ノードではない)のmiddleイベントを発生させます。
    3. ネイバーを訪問できる場合は、ネイバーを訪問します。MAX_THRESHOLD訪問可能性チェックは、隣人が a)回 (疑似無限サイクルの場合) および b) 上記で計算された繰り返しカウント回数未満で訪問されたかどうかをチェックすることによって行われます。
  5. これでこのノードは完了です。イベントを発生させend、ハッシュテーブルでその訪問をクリアします。これは、他のノードがそれを再度呼び出した場合に、訪問可能性チェックに失敗したり、必要な回数未満で実行したりしないようにするためです。
于 2016-09-12T17:54:10.930 に答える
1

コメント 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

グラフと を使用するとdef process(node): print node、次のようになります。

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

もう 1 つの BFS アルゴリズムである反復深化 DFS (速度を犠牲にして少ないメモリを使用する) は、この場合、何の得にもなりません: 訪問したノードへの参照を保存する必要があるため、既にO(n)メモリを消費しています。どちらも中間結果を生成する必要はありません (しかしyield、レベルを処理した後の何かなど)。

于 2016-09-13T08:27:56.187 に答える