web-dev-qa-db-ja.com

python

here から抽出した最小の反復dfsルーチンを取得しました。コードをさらに単純化することはほとんどできないため、これを最小と呼びます。

def iterative_dfs(graph, start, path=[]):
    q = [start]
    while q:
        v = q.pop(0)
        if v not in path:
            path = path + [v]
            q = graph[v] + q

    return path

graph = {
    'a': ['b', 'c'],
    'b': ['d'],
    'c': ['d'],
    'd': ['e'],
    'e': []
}
print(iterative_dfs(graph, 'a'))

ここに私の質問があります。どのようにして、このルーチンを「最小」になるトポロジカルなソート方法に変換できますか?私はこれを見ました video アイデアは非常に賢いので、上記のコードに同じトリックを適用して、topological_sortの最終結果も「最小」になるかどうか疑問に思っていました。

上記のルーチンの小さな修正ではないトポロジソートのバージョンを要求しないので、それらのいくつかは既に見ました。問題は「どのようにPythonでトポロジカルソートを実装するか」ではなく、代わりに、上記のコードの可能な限り最小の調整セットを見つけてtopological_sortになることです。

追加コメント

元の記事では、著者は次のように述べています。

少し前に、Guido van Rossenによる一見シンプルなグラフ実装を読みました。今、私は純粋なpython最小の複雑さを備えた最小限のシステムを主張します。アイデアはアルゴリズムを探索できるようにすることです。後で、コードを改良および最適化できますが、おそらくコンパイルされた言語でこれを行います。

この質問の目標は、iterative_dfsを最適化することではなく、それから派生したtopological_sortの最小バージョンを考え出すことです(グラフ理論アルゴリズムについての詳細を学ぶためです)。実際、より一般的な質問は、最小アルゴリズムのセット{iterative_dfsrecursive_dfsiterative_bfsrecursive_dfs}のようなものであると思います。 topological_sort派生?それは質問をより長く/複雑にしますが、iterative_dfsからtopological_sortを見つけることで十分です。

19
BPL

DFSの反復実装をトポロジカルソートに変換するのは簡単ではありません。実行する必要がある変更は、再帰的な実装の方が自然だからです。ただし、それでも実行できます。独自のスタックを実装するだけです。

まず、コードを少し改善したバージョンを示します(はるかに効率的で、それほど複雑ではありません):

_def iterative_dfs_improved(graph, start):
    seen = set()  # efficient set to look up nodes in
    path = []     # there was no good reason for this to be an argument in your code
    q = [start]
    while q:
        v = q.pop()   # no reason not to pop from the end, where it's fast
        if v not in seen:
            seen.add(v)
            path.append(v)
            q.extend(graph[v]) # this will add the nodes in a slightly different order
                               # if you want the same order, use reversed(graph[v])

    return path
_

トポロジーソートを行うためにそのコードを変更する方法は次のとおりです。

_def iterative_topological_sort(graph, start):
    seen = set()
    stack = []    # path variable is gone, stack and order are new
    order = []    # order will be in reverse order at first
    q = [start]
    while q:
        v = q.pop()
        if v not in seen:
            seen.add(v) # no need to append to path any more
            q.extend(graph[v])

            while stack and v not in graph[stack[-1]]: # new stuff here!
                order.append(stack.pop())
            stack.append(v)

    return stack + order[::-1]   # new return value!
_

「ここに新しいもの」とコメントした部分は、スタックを上に移動するときに順序を把握する部分です。見つかった新しいノードが前のノード(スタックの一番上)の子であるかどうかを確認します。そうでない場合、スタックの最上部をポップし、orderに値を追加します。 DFSを実行している間、orderは最後の値から始まり、逆のトポロジの順序になります。関数の最後でそれを逆にし、スタック上の残りの値と連結します(これらは既に正しい順序になっています)。

このコードは_v not in graph[stack[-1]]_を何度もチェックする必要があるため、graph辞書の値がリストではなく設定されていると、はるかに効率的です。グラフは通常、エッジの保存順序を考慮しないため、グラフを作成または更新するコードの修正が必要になる場合がありますが、そのような変更を行っても他のほとんどのアルゴリズムで問題は発生しません。重み付きグラフをサポートするためにグラフコードを拡張する予定がある場合、おそらくリストをノードから重みにマッピングする辞書に変更することになり、このコードでも同様に機能します(辞書検索はO(1)セット検索のように)。または、graphを直接変更できない場合は、必要なセットを作成できます。

参考のために、ここにDFSの再帰バージョンと、トポロジカルソートを行うための修正を示します。実際に必要な変更は非常にわずかです。

_def recursive_dfs(graph, node):
    result = []
    seen = set()

    def recursive_helper(node):
        for neighbor in graph[node]:
            if neighbor not in seen:
                result.append(neighbor)     # this line will be replaced below
                seen.add(neighbor)
                recursive_helper(neighbor)

    recursive_helper(node)
    return result

def recursive_topological_sort(graph, node):
    result = []
    seen = set()

    def recursive_helper(node):
        for neighbor in graph[node]:
            if neighbor not in seen:
                seen.add(neighbor)
                recursive_helper(neighbor)
        result.insert(0, node)              # this line replaces the result.append line

    recursive_helper(node)
    return result
_

それでおしまい! 1つの行が削除され、同様の行が別の場所に追加されます。パフォーマンスに関心がある場合は、おそらく2番目のヘルパー関数でも_result.append_を実行し、トップレベルの_return result[::-1]_関数で_recursive_topological_sort_を実行する必要があります。ただし、insert(0, ...)を使用することは、最小限の変更です。

グラフ全体のトポロジカル順序が必要な場合は、開始ノードを指定する必要がないことにも注意してください。実際、グラフ全体をトラバースできる単一のノードは存在しない可能性があるため、すべてに到達するにはいくつかのトラバースを行う必要がある場合があります。反復トポロジカルソートでこれを簡単に行う方法は、開始ノードが1つだけのリストではなく、qlist(graph)(すべてのグラフのキーのリスト)に初期化することです。再帰バージョンの場合、recursive_helper(node)への呼び出しを、seenにない場合はグラフ内のすべてのノードでヘルパー関数を呼び出すループに置き換えます。

22
Blckknght

私のアイデアは、2つの重要な観察に基づいています。

  1. スタックから次のアイテムをポップしないでください。スタックのアンワインドをエミュレートするためにそれを保持してください。
  2. すべての子をスタックにプッシュする代わりに、1つだけプッシュします。

これらは両方とも、再帰的なdfのようにグラフを走査するのに役立ちます。ここで述べた他の答えとして、これはこの特定の問題にとって重要です。残りは簡単なはずです。

def iterative_topological_sort(graph, start,path=set()):
    q = [start]
    ans = []
    while q:
        v = q[-1]                   #item 1,just access, don't pop
        path = path.union({v})  
        children = [x for x in graph[v] if x not in path]    
        if not children:              #no child or all of them already visited
            ans = [v]+ans 
            q.pop()
        else: q.append(children[0])   #item 2, Push just one child

    return ans

qはスタックです。メインループでは、現在のノードvをスタックから「アクセス」します。このノードに再びアクセスできるようにする必要があるため、「ポップ」ではなく「アクセス」。現在のノードの未訪問の子をすべて見つけます。そして、すべてを一緒にスタックするのではなく、最初のスタックのみをプッシュします(q.append(children[0]))。繰り返しますが、これはまさに再帰dfを使用して行うことです。

対象の子が見つからない場合(if not children)、その下のサブツリー全体を訪問しました。これで、ansにプッシュする準備が整いました。そして、これは私たちが本当にそれをポップするときです。

(言うまでもなく、パフォーマンス面では素晴らしいことではありません。children変数ですべての未訪問の子を生成する代わりに、フィルターを使用して最初のジェネレータースタイルを生成する必要があります。ans = [v] + ansreverseの最後にansがあります。しかし、OPが単純さを主張するため、これらのことは省略されています。)