Poiché l'implementazione DFS non ricorsiva esistente fornita in questa risposta sembra essere interrotta, consentitemi di fornirne una che funzioni effettivamente.
L'ho scritto in Python, perché lo trovo abbastanza leggibile e ordinato dai dettagli di implementazione (e perché ha la comoda yield
parola chiave per l'implementazione dei generatori ), ma dovrebbe essere abbastanza facile portarlo in altri linguaggi.
# 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
Questo codice mantiene due stack paralleli: uno contenente i nodi precedenti nel percorso corrente e uno contenente l'indice del vicino corrente per ogni nodo nello stack del nodo (in modo che possiamo riprendere l'iterazione attraverso i nodi vicini di un nodo quando lo ripristiniamo la pila). Avrei potuto usare ugualmente bene un singolo stack di coppie (nodo, indice), ma ho pensato che il metodo a due stack sarebbe stato più leggibile e forse più facile da implementare per gli utenti di altre lingue.
Questo codice utilizza anche un visited
set separato , che contiene sempre il nodo corrente e tutti i nodi sullo stack, per consentirmi di controllare in modo efficiente se un nodo fa già parte del percorso corrente. Se la lingua capita di avere un "insieme ordinato" struttura dati che fornisce sia efficiente stack come push / pop operazioni e query di appartenenza efficienti, è possibile utilizzare che per lo stack nodo e sbarazzarsi della separata visited
insieme.
In alternativa, se stai utilizzando una classe / struttura modificabile personalizzata per i tuoi nodi, puoi semplicemente memorizzare un flag booleano in ogni nodo per indicare se è stato visitato come parte del percorso di ricerca corrente. Ovviamente, questo metodo non ti consentirà di eseguire due ricerche sullo stesso grafico in parallelo, se per qualche motivo desideri farlo.
Ecco un po 'di codice di prova che dimostra come funziona la funzione sopra indicata:
# 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)
L'esecuzione di questo codice sul grafico di esempio fornito produce il seguente output:
A -> B -> C -> D
A -> B -> D
A -> C -> B -> D
A -> C -> D
Si noti che, sebbene questo grafico di esempio non sia orientato (cioè tutti i suoi bordi vanno in entrambe le direzioni), l'algoritmo funziona anche per grafici diretti arbitrari. Ad esempio, la rimozione del C -> B
bordo (rimuovendo B
dall'elenco dei vicini di C
) restituisce lo stesso output eccetto il terzo percorso ( A -> C -> B -> D
), che non è più possibile.
Ps. È facile costruire grafici per i quali semplici algoritmi di ricerca come questo (e gli altri dati in questo thread) funzionano molto male.
Ad esempio, si consideri il compito di trovare tutti i percorsi da A a B su un grafo non orientato in cui il nodo iniziale A ha due vicini: il nodo obiettivo B (che non ha altri vicini oltre A) e un nodo C che fa parte di una cricca di n +1 nodi, in questo modo:
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"),
}
È facile vedere che l'unico percorso tra A e B è quello diretto, ma un ingenuo DFS avviato dal nodo A sprecherà O ( n !) Tempo esplorando inutilmente percorsi all'interno della cricca, anche se è ovvio (per un umano) che nessuno di questi percorsi può portare a B.
Si possono anche costruire DAG con proprietà simili, ad esempio facendo in modo che il nodo di partenza A colleghi il nodo di destinazione B e altri due nodi C 1 e C 2 , entrambi collegati ai nodi D 1 e D 2 , entrambi collegati a E 1 ed E 2 e così via. Per n strati di nodi disposti in questo modo, una ricerca ingenua di tutti i percorsi da A a B finirà per far perdere O (2 n ) tempo ad esaminare tutti i possibili vicoli ciechi prima di arrendersi.
Naturalmente, l'aggiunta di un bordo al nodo di destinazione B da uno dei nodi della cricca (oltre che C), o dall'ultima strato del DAG, potrebbe creare un esponenziale gran numero di possibili percorsi da A a B, e l'algoritmo di ricerca puramente locale non può davvero dire in anticipo se troverà un tale vantaggio o meno. Quindi, in un certo senso, la scarsa sensibilità all'output di tali ricerche ingenue è dovuta alla loro mancanza di consapevolezza della struttura globale del grafico.
Sebbene esistano vari metodi di preelaborazione (come l'eliminazione iterativa dei nodi foglia, la ricerca di separatori di vertici a nodo singolo, ecc.) Che potrebbero essere utilizzati per evitare alcuni di questi "vicoli ciechi del tempo esponenziale", non conosco nessun generale trucco di preelaborazione che potrebbe eliminarli in tutti i casi. Una soluzione generale sarebbe controllare in ogni fase della ricerca se il nodo di destinazione è ancora raggiungibile (utilizzando una sotto-ricerca) e tornare indietro in anticipo se non lo è, ma ahimè, ciò rallenterebbe notevolmente la ricerca (nel peggiore dei casi , proporzionalmente alla dimensione del grafico) per molti grafici che non contengono tali vicoli ciechi patologici.