この回答で示されている既存の非再帰的なDFS実装は壊れているようですので、実際に機能するものを提供しましょう。
私はこれをPythonで記述しました。実装の詳細によって読みやすく、整頓されているため(そしてジェネレーターyield
を実装するための便利なキーワードがあるため)、他の言語への移植はかなり簡単なはずです。
# a generator function to find all simple paths between two nodes in a
# graph, represented as a dictionary that maps nodes to their neighbors
def find_simple_paths(graph, start, end):
visited = set()
visited.add(start)
nodestack = list()
indexstack = list()
current = start
i = 0
while True:
# get a list of the neighbors of the current node
neighbors = graph[current]
# find the next unvisited neighbor of this node, if any
while i < len(neighbors) and neighbors[i] in visited: i += 1
if i >= len(neighbors):
# we've reached the last neighbor of this node, backtrack
visited.remove(current)
if len(nodestack) < 1: break # can't backtrack, stop!
current = nodestack.pop()
i = indexstack.pop()
elif neighbors[i] == end:
# yay, we found the target node! let the caller process the path
yield nodestack + [current, end]
i += 1
else:
# push current node and index onto stacks, switch to neighbor
nodestack.append(current)
indexstack.append(i+1)
visited.add(neighbors[i])
current = neighbors[i]
i = 0
このコードは2つの並列スタックを維持します。1つは現在のパスの以前のノードを含み、もう1つはノードスタックの各ノードの現在のネイバーインデックスを含みます(これにより、ノードをポップオフしたときにノードのネイバーの反復を再開できます。スタック)。(ノード、インデックス)ペアの単一スタックを同様に使用することもできましたが、2スタック方式の方が読みやすく、おそらく他の言語のユーザーが実装しやすいと考えました。
このコードvisited
は、現在のノードとスタック上のノードを常に含む別のセットも使用して、ノードが既に現在のパスの一部であるかどうかを効率的に確認できるようにしています。言語に、効率的なスタックのようなプッシュ/ポップ操作と効率的なメンバーシップクエリの両方を提供する「順序付けられたセット」のデータ構造がある場合、それをノードスタックに使用して、個別のvisited
セットを取り除くことができます。
または、ノードにカスタムの可変クラス/構造を使用している場合は、各ノードにブールフラグを格納して、現在の検索パスの一部としてアクセスされたかどうかを示すことができます。もちろん、この方法では、何らかの理由で同じグラフに対して2つの検索を並行して実行することはできません。
上記の関数がどのように機能するかを示すテストコードを次に示します。
# test graph:
# ,---B---.
# A | D
# `---C---'
graph = {
"A": ("B", "C"),
"B": ("A", "C", "D"),
"C": ("A", "B", "D"),
"D": ("B", "C"),
}
# find paths from A to D
for path in find_simple_paths(graph, "A", "D"): print " -> ".join(path)
与えられた例のグラフでこのコードを実行すると、次の出力が生成されます。
A-> B-> C-> D
A-> B-> D
A-> C-> B-> D
A-> C-> D
このサンプルグラフは無向(つまり、すべてのエッジが双方向)ですが、アルゴリズムは任意の有向グラフに対しても機能することに注意してください。たとえば、C -> B
(B
のネイバーリストから削除することにより)エッジを削除するC
と、3番目のパス(A -> C -> B -> D
)を除いて同じ出力が生成されますが、これは不可能です。
Ps。このような単純な検索アルゴリズム(およびこのスレッドで提供されるその他のアルゴリズム)のパフォーマンスが非常に低いグラフを作成するのは簡単です。
たとえば、開始ノードAに2つの隣接ノードがある無向グラフ上でAからBへのすべてのパスを検索するタスクを考えてみます。ターゲットノードB(A以外の隣接ノードはありません)とクリークの一部であるノードC 次のようにn +1ノードの:
graph = {
"A": ("B", "C"),
"B": ("A"),
"C": ("A", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"D": ("C", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"E": ("C", "D", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"F": ("C", "D", "E", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"G": ("C", "D", "E", "F", "H", "I", "J", "K", "L", "M", "N", "O"),
"H": ("C", "D", "E", "F", "G", "I", "J", "K", "L", "M", "N", "O"),
"I": ("C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "O"),
"J": ("C", "D", "E", "F", "G", "H", "I", "K", "L", "M", "N", "O"),
"K": ("C", "D", "E", "F", "G", "H", "I", "J", "L", "M", "N", "O"),
"L": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "M", "N", "O"),
"M": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "N", "O"),
"N": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "O"),
"O": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N"),
}
AとBの間の唯一のパスが直接パスであることは簡単にわかりますが、ノードAから開始された単純なDFSは、O(n!)時間を無駄にします。これらのパスのいずれもBにつながる可能性はありません。
一つも構築することができるのDAG開始ノードAの接続ターゲットノードBと他の2つのノードCに有することにより、例えば類似の特性を有するが1とC 2のノードに接続どちらもが、D 1およびD 2 Eに接続どちらも、1とE 2など。以下のために、Nのノードの層がこのように配置され、BのAからのすべてのパスのためのナイーブ検索はO(2無駄にしてしまうNあきらめる前に、すべての可能な行き止まりを調べる)時間。
もちろん、(C以外)クリーク内のノードのいずれかからターゲットノードBにエッジを追加する、またはDAGの最後の層からなる指数関数的に大きいBのAからの可能な経路の数、及びAを作成します純粋なローカル検索アルゴリズムは、そのようなエッジを見つけるかどうかを事前に実際に判断することはできません。したがって、ある意味で、このような単純な検索の出力感度が低いのは、グラフのグローバル構造を認識していないためです。
これらの「指数時間の行き止まり」の一部を回避するために使用できるさまざまな前処理方法(リーフノードの反復的な削除、単一ノードの頂点セパレーターの検索など)がありますが、一般的なことはわかりませんすべてのケースでそれらを排除できる前処理のトリック。一般的な解決策は、ターゲットノードがまだ到達可能かどうかを(サブサーチを使用して)検索のすべてのステップでチェックし、到達できない場合は早期にバックトラックすることです。 、グラフのサイズに比例します)このような病理学的行き止まりを含まない多くのグラフ。