Da die in dieser Antwort angegebene nicht rekursive DFS-Implementierung fehlerhaft zu sein scheint, möchte ich eine bereitstellen, die tatsächlich funktioniert.
Ich habe dies in Python geschrieben, weil ich es durch Implementierungsdetails ziemlich lesbar und übersichtlich finde (und weil es das praktische yield
Schlüsselwort zum Implementieren von Generatoren enthält ), aber es sollte ziemlich einfach sein, es in andere Sprachen zu portieren.
# 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
Dieser Code verwaltet zwei parallele Stapel: einen mit den früheren Knoten im aktuellen Pfad und einen mit dem aktuellen Nachbarindex für jeden Knoten im Knotenstapel (sodass wir die Iteration durch die Nachbarn eines Knotens fortsetzen können, wenn wir ihn wieder entfernen der Stapel). Ich hätte genauso gut einen einzelnen Stapel von (Knoten-, Index-) Paaren verwenden können, aber ich dachte, die Zwei-Stapel-Methode wäre besser lesbar und für Benutzer anderer Sprachen möglicherweise einfacher zu implementieren.
Dieser Code verwendet auch einen separaten visited
Satz, der immer den aktuellen Knoten und alle Knoten auf dem Stapel enthält, damit ich effizient prüfen kann, ob ein Knoten bereits Teil des aktuellen Pfads ist. Wenn Ihre Sprache zufällig über eine Datenstruktur "geordneter Satz" verfügt, die sowohl effiziente stapelähnliche Push / Pop-Operationen als auch effiziente Mitgliedschaftsabfragen bietet, können Sie diese für den Knotenstapel verwenden und den separaten visited
Satz entfernen.
Wenn Sie alternativ eine benutzerdefinierte veränderbare Klasse / Struktur für Ihre Knoten verwenden, können Sie einfach ein boolesches Flag in jedem Knoten speichern, um anzugeben, ob es als Teil des aktuellen Suchpfads besucht wurde. Natürlich können Sie mit dieser Methode nicht zwei Suchvorgänge im selben Diagramm gleichzeitig ausführen, falls Sie dies aus irgendeinem Grund wünschen.
Hier ist ein Testcode, der zeigt, wie die oben angegebene Funktion funktioniert:
# 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)
Wenn Sie diesen Code in dem angegebenen Beispieldiagramm ausführen, wird die folgende Ausgabe ausgegeben:
A -> B -> C -> D.
A -> B -> D.
A -> C -> B -> D.
A -> C -> D.
Beachten Sie, dass dieses Beispieldiagramm zwar ungerichtet ist (dh alle Kanten in beide Richtungen verlaufen), der Algorithmus jedoch auch für beliebig gerichtete Diagramme funktioniert. Wenn Sie beispielsweise die C -> B
Kante entfernen (indem Sie sie B
aus der Nachbarliste von entfernen C
), erhalten Sie dieselbe Ausgabe, mit Ausnahme des dritten Pfads ( A -> C -> B -> D
), der nicht mehr möglich ist.
Ps. Es ist einfach, Diagramme zu erstellen, für die einfache Suchalgorithmen wie dieser (und die anderen in diesem Thread) sehr schlecht funktionieren.
Betrachten Sie beispielsweise die Aufgabe, alle Pfade von A nach B in einem ungerichteten Diagramm zu finden, in dem der Startknoten A zwei Nachbarn hat: den Zielknoten B (der keine anderen Nachbarn als A hat) und einen Knoten C, der Teil einer Clique ist von n + 1 Knoten, wie folgt:
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"),
}
Es ist leicht zu erkennen, dass der einzige Pfad zwischen A und B der direkte ist, aber eine naive DFS, die von Knoten A aus gestartet wird, verschwendet O ( n !) Zeit, um Pfade innerhalb der Clique nutzlos zu erkunden, obwohl dies (für einen Menschen) offensichtlich ist Keiner dieser Pfade kann möglicherweise zu B führen.
Man kann auch DAGs mit ähnlichen Eigenschaften konstruieren , z. B. indem der Startknoten A den Zielknoten B und zwei andere Knoten C 1 und C 2 verbindet , die beide mit den Knoten D 1 und D 2 verbunden sind , die beide mit E verbunden sind 1 und E 2 und so weiter. Für n Schichten von Knoten, die so angeordnet sind, verschwendet eine naive Suche nach allen Pfaden von A nach B O (2 n ) Zeit, um alle möglichen Sackgassen zu untersuchen, bevor sie aufgegeben wird.
Das Hinzufügen einer Kante zum Zielknoten B von einem der Knoten in der Clique (außer C) oder von der letzten Schicht der DAG würde natürlich eine exponentiell große Anzahl möglicher Pfade von A nach B und a erzeugen Ein rein lokaler Suchalgorithmus kann nicht wirklich im Voraus sagen, ob er eine solche Kante findet oder nicht. In gewissem Sinne ist die schlechte Ausgabesensitivität solcher naiven Suchvorgänge darauf zurückzuführen, dass sie sich der globalen Struktur des Graphen nicht bewusst sind.
Zwar gibt es verschiedene Vorverarbeitungsmethoden (z. B. iteratives Entfernen von Blattknoten, Suchen nach Scheitelpunkttrennzeichen für einzelne Knoten usw.), mit denen einige dieser "Sackgassen in Exponentialzeit" vermieden werden können, aber ich kenne keine allgemeinen Vorverarbeitungstrick, der sie in allen Fällen beseitigen könnte . Eine allgemeine Lösung wäre, bei jedem Schritt der Suche zu überprüfen, ob der Zielknoten noch erreichbar ist (mithilfe einer Untersuche), und frühzeitig zurückzuverfolgen, wenn dies nicht der Fall ist - aber leider würde dies die Suche erheblich verlangsamen (im schlimmsten Fall) (proportional zur Größe des Diagramms) für viele Diagramme, die keine solchen pathologischen Sackgassen enthalten.