Étant donné que l'implémentation DFS non récursive existante donnée dans cette réponse semble être interrompue, permettez-moi de vous en fournir une qui fonctionne réellement.
J'ai écrit ceci en Python, parce que je le trouve assez lisible et épuré par les détails d'implémentation (et parce qu'il a le yield
mot-clé pratique pour implémenter des générateurs ), mais il devrait être assez facile de porter vers d'autres langages.
# 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
Ce code maintient deux piles parallèles: l'une contenant les nœuds précédents dans le chemin actuel, et l'autre contenant l'index actuel du voisin pour chaque nœud de la pile de nœuds (afin que nous puissions reprendre l'itération à travers les voisins d'un nœud lorsque nous le faisons sauter la pile). J'aurais tout aussi bien pu utiliser une seule pile de paires (nœud, index), mais j'ai pensé que la méthode à deux piles serait plus lisible et peut-être plus facile à implémenter pour les utilisateurs d'autres langues.
Ce code utilise également un visited
ensemble séparé , qui contient toujours le nœud actuel et tous les nœuds de la pile, pour me permettre de vérifier efficacement si un nœud fait déjà partie du chemin actuel. Si votre langage possède une structure de données «ensemble ordonné» qui fournit à la fois des opérations push / pop efficaces de type pile et des requêtes d'appartenance efficaces, vous pouvez l'utiliser pour la pile de nœuds et vous débarrasser de l' visited
ensemble séparé .
Alternativement, si vous utilisez une classe / structure mutable personnalisée pour vos nœuds, vous pouvez simplement stocker un indicateur booléen dans chaque nœud pour indiquer s'il a été visité dans le cadre du chemin de recherche actuel. Bien sûr, cette méthode ne vous permettra pas d'exécuter deux recherches sur le même graphique en parallèle, si vous souhaitez le faire pour une raison quelconque.
Voici un code de test démontrant le fonctionnement de la fonction ci-dessus:
# 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'exécution de ce code sur le graphique d'exemple donné produit la sortie suivante:
A -> B -> C -> D
A -> B -> D
A -> C -> B -> D
A -> C -> D
Notez que, bien que cet exemple de graphe ne soit pas orienté (c'est-à-dire que toutes ses arêtes vont dans les deux sens), l'algorithme fonctionne également pour les graphes dirigés arbitraires. Par exemple, la suppression du C -> B
bord (en supprimant B
de la liste des voisins de C
) donne le même résultat sauf pour le troisième chemin ( A -> C -> B -> D
), ce qui n'est plus possible.
Ps. Il est facile de construire des graphiques pour lesquels des algorithmes de recherche simples comme celui-ci (et les autres donnés dans ce fil) fonctionnent très mal.
Par exemple, considérons la tâche de trouver tous les chemins de A à B sur un graphe non orienté où le nœud de départ A a deux voisins: le nœud cible B (qui n'a pas d'autres voisins que A) et un nœud C qui fait partie d'une clique de n +1 nœuds, comme ceci:
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"),
}
Il est facile de voir que le seul chemin entre A et B est le chemin direct, mais un DFS naïf démarré à partir du nœud A perdra O ( n !) Temps à explorer inutilement les chemins au sein de la clique, même s'il est évident (pour un humain) que aucun de ces chemins ne peut conduire à B.
On peut également construire des DAG avec des propriétés similaires, par exemple en demandant au nœud de départ A de connecter le nœud cible B et à deux autres nœuds C 1 et C 2 , tous deux se connectant aux nœuds D 1 et D 2 , qui se connectent tous deux à E 1 et E 2 , et ainsi de suite. Pour n couches de nœuds disposés comme ceci, une recherche naïve de tous les chemins de A à B finira par gaspiller O (2 n ) temps à examiner toutes les impasses possibles avant d'abandonner.
Bien entendu, l' ajout d' un bord vers le noeud cible B à partir de l' un des noeuds dans la clique (autre que C), ou à partir de la dernière couche du DAG, serait de créer un nombre exponentiellement grand nombre de chemins possibles de A à B, et L'algorithme de recherche purement local ne peut pas vraiment dire à l'avance s'il trouvera un tel avantage ou non. Ainsi, dans un sens, la faible sensibilité de sortie de ces recherches naïves est due à leur manque de conscience de la structure globale du graphique.
Bien qu'il existe diverses méthodes de prétraitement (telles que l'élimination itérative des nœuds feuilles, la recherche de séparateurs de sommets à un seul nœud, etc.) qui pourraient être utilisées pour éviter certaines de ces "impasses à temps exponentiel", je ne connais aucun général astuce de prétraitement qui pourrait les éliminer dans tous les cas. Une solution générale serait de vérifier à chaque étape de la recherche si le nœud cible est toujours accessible (en utilisant une sous-recherche), et de revenir en arrière tôt si ce n'est pas le cas - mais hélas, cela ralentirait considérablement la recherche (au pire , proportionnellement à la taille du graphique) pour de nombreux graphiques qui ne contiennent pas de telles impasses pathologiques.