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_dfs
、recursive_dfs
、iterative_bfs
、recursive_dfs
}のようなものであると思います。 topological_sort派生?それは質問をより長く/複雑にしますが、iterative_dfsからtopological_sortを見つけることで十分です。
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つだけのリストではなく、q
をlist(graph)
(すべてのグラフのキーのリスト)に初期化することです。再帰バージョンの場合、recursive_helper(node)
への呼び出しを、seen
にない場合はグラフ内のすべてのノードでヘルパー関数を呼び出すループに置き換えます。
私のアイデアは、2つの重要な観察に基づいています。
これらは両方とも、再帰的な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] + ans
reverse
の最後にans
があります。しかし、OPが単純さを主張するため、これらのことは省略されています。)