Ponieważ istniejąca nierekurencyjna implementacja DFS podana w tej odpowiedzi wydaje się być zepsuta, pozwólcie, że przedstawię taką, która faktycznie działa.
Napisałem to w Pythonie, ponieważ uważam, że jest dość czytelny i niezakłócony szczegółami implementacji (i ponieważ zawiera przydatne yield
słowo kluczowe do implementacji generatorów ), ale powinno być dość łatwe do przeniesienia na inne języki.
# 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
Ten kod utrzymuje dwa równoległe stosy: jeden zawierający wcześniejsze węzły w bieżącej ścieżce i jeden zawierający bieżący indeks sąsiadów dla każdego węzła w stosie węzłów (abyśmy mogli wznowić iterację przez sąsiadów węzła, gdy go wycofamy stos). Równie dobrze mogłem użyć pojedynczego stosu par (węzeł, indeks), ale pomyślałem, że metoda z dwoma stosami byłaby bardziej czytelna i być może łatwiejsza do wdrożenia dla użytkowników innych języków.
Ten kod używa również oddzielnego visited
zestawu, który zawsze zawiera bieżący węzeł i wszystkie węzły na stosie, aby umożliwić mi efektywne sprawdzenie, czy węzeł jest już częścią bieżącej ścieżki. Jeśli zdarzy się, że Twój język ma strukturę danych „uporządkowanego zestawu”, która zapewnia zarówno wydajne operacje push / pop podobne do stosu, jak i wydajne zapytania członkostwa, możesz użyć tego dla stosu węzłów i pozbyć się oddzielnego visited
zestawu.
Alternatywnie, jeśli używasz niestandardowej mutowalnej klasy / struktury dla swoich węzłów, możesz po prostu przechowywać flagę logiczną w każdym węźle, aby wskazać, czy został odwiedzony jako część bieżącej ścieżki wyszukiwania. Oczywiście ta metoda nie pozwoli ci przeprowadzić równolegle dwóch wyszukiwań na tym samym wykresie, jeśli z jakiegoś powodu chcesz to zrobić.
Oto kod testowy demonstrujący działanie funkcji podanej powyżej:
# 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)
Uruchomienie tego kodu na podanym przykładowym wykresie daje następujące dane wyjściowe:
A -> B -> C -> D
A -> B -> D
A -> C -> B -> D
A -> C -> D
Zauważ, że chociaż ten przykładowy graf jest nieukierunkowany (tj. Wszystkie jego krawędzie idą w obie strony), algorytm działa również dla dowolnie ukierunkowanych grafów. Na przykład usunięcie C -> B
krawędzi (przez usunięcie B
z listy sąsiadów C
) daje takie same wyniki, z wyjątkiem trzeciej ścieżki ( A -> C -> B -> D
), która nie jest już możliwa.
Ps. Łatwo jest skonstruować wykresy, dla których proste algorytmy wyszukiwania, takie jak ten (i inne podane w tym wątku), działają bardzo słabo.
Na przykład rozważmy zadanie znalezienia wszystkich ścieżek od A do B na grafie niekierowanym, gdzie węzeł początkowy A ma dwóch sąsiadów: węzeł docelowy B (który nie ma innych sąsiadów niż A) i węzeł C będący częścią kliki. z n +1 węzłów, na przykład:
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"),
}
Łatwo zauważyć, że jedyna ścieżka między A i B jest bezpośrednia, ale naiwny DFS uruchomiony z węzła A zmarnuje O ( n !) Czas bezużytecznie eksplorując ścieżki wewnątrz kliki, mimo że jest oczywiste (dla człowieka), że żadna z tych ścieżek nie może prowadzić do B.
Można również konstruować DAG o podobnych właściwościach, np. Poprzez połączenie węzła początkowego A węzła docelowego B i dwóch innych węzłów C 1 i C 2 , z których oba łączą się z węzłami D 1 i D 2 , z których oba łączą się z E 1 i E 2 i tak dalej. Dla n warstw węzłów ułożonych w ten sposób naiwne poszukiwanie wszystkich ścieżek od A do B zakończy się marnowaniem O (2 n ) czasu na zbadanie wszystkich możliwych ślepych uliczek, zanim się poddaje.
Oczywiście dodanie krawędzi do węzła docelowego B z jednym z węzłów w klika (inne niż C) lub od ostatniej warstwy DAG, by utworzyć wykładniczo dużą liczbę możliwych ścieżek z A do B, a czysto lokalny algorytm wyszukiwania nie jest w stanie z góry powiedzieć, czy znajdzie taką krawędź, czy nie. Zatem w pewnym sensie niska wrażliwość wyników takich naiwnych wyszukiwań wynika z braku świadomości globalnej struktury wykresu.
Chociaż istnieją różne metody przetwarzania wstępnego (takie jak iteracyjne eliminowanie węzłów liści, wyszukiwanie separatorów wierzchołków pojedynczych węzłów itp.), Których można by użyć do uniknięcia niektórych z tych „ślepych zaułków w czasie wykładniczym”, nie znam żadnych ogólnych sztuczka z przetwarzaniem wstępnym, która może je wyeliminować we wszystkich przypadkach. Ogólnym rozwiązaniem byłoby sprawdzenie na każdym etapie wyszukiwania, czy węzeł docelowy jest nadal osiągalny (za pomocą wyszukiwania podrzędnego) i cofnięcie się wcześniej, jeśli tak nie jest - ale niestety, to znacznie spowolniłoby wyszukiwanie (w najgorszym przypadku proporcjonalnie do wielkości wykresu) dla wielu wykresów, które nie zawierają takich patologicznych ślepych zaułków.