Dado que la implementación de DFS no recursiva existente que se proporciona en esta respuesta parece estar rota, permítanme proporcionar una que realmente funcione.
Escribí esto en Python, porque lo encuentro bastante legible y ordenado por los detalles de implementación (y porque tiene la yield
palabra clave útil para implementar generadores ), pero debería ser bastante fácil de migrar a otros lenguajes.
# 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
Este código mantiene dos pilas paralelas: una que contiene los nodos anteriores en la ruta actual y otra que contiene el índice vecino actual para cada nodo en la pila de nodos (para que podamos reanudar la iteración a través de los vecinos de un nodo cuando lo sacamos la pila). Podría haber usado igualmente una sola pila de pares (nodo, índice), pero pensé que el método de dos pilas sería más legible y quizás más fácil de implementar para los usuarios de otros idiomas.
Este código también usa un visited
conjunto separado , que siempre contiene el nodo actual y cualquier nodo en la pila, para permitirme verificar de manera eficiente si un nodo ya es parte de la ruta actual. Si su lenguaje tiene una estructura de datos de "conjunto ordenado" que proporciona tanto operaciones push / pop eficientes como una pila y consultas de membresía eficientes, puede usar eso para la pila de nodos y deshacerse del visited
conjunto separado .
Alternativamente, si está utilizando una clase / estructura mutable personalizada para sus nodos, puede simplemente almacenar una marca booleana en cada nodo para indicar si se ha visitado como parte de la ruta de búsqueda actual. Por supuesto, este método no le permitirá ejecutar dos búsquedas en el mismo gráfico en paralelo, si por alguna razón desea hacerlo.
Aquí hay un código de prueba que demuestra cómo funciona la función dada anteriormente:
# 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)
Ejecutar este código en el gráfico de ejemplo dado produce el siguiente resultado:
A -> B -> C -> D
A -> B -> D
A -> C -> B -> D
A -> C -> D
Tenga en cuenta que, si bien este gráfico de ejemplo no está dirigido (es decir, todos sus bordes van en ambos sentidos), el algoritmo también funciona para gráficos dirigidos arbitrarios. Por ejemplo, eliminar el C -> B
borde (eliminando B
de la lista de vecinos de C
) produce el mismo resultado excepto por la tercera ruta ( A -> C -> B -> D
), que ya no es posible.
PD. Es fácil construir gráficos para los cuales algoritmos de búsqueda simples como este (y los otros que se dan en este hilo) funcionan muy mal.
Por ejemplo, considere la tarea de encontrar todas las rutas de A a B en un gráfico no dirigido donde el nodo inicial A tiene dos vecinos: el nodo objetivo B (que no tiene otros vecinos que A) y un nodo C que es parte de una camarilla. de n +1 nodos, así:
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 fácil ver que el único camino entre A y B es el directo, pero un DFS ingenuo iniciado desde el nodo A desperdiciará O ( ¡ n !) Tiempo explorando inútilmente caminos dentro de la camarilla, aunque es obvio (para un humano) que ninguno de esos caminos puede conducir posiblemente a B.
También se pueden construir DAG con propiedades similares, por ejemplo, haciendo que el nodo inicial A conecte el nodo objetivo B y a otros dos nodos C 1 y C 2 , los cuales se conectan a los nodos D 1 y D 2 , los cuales se conectan a E 1 y E 2 , y así sucesivamente. Para n capas de nodos organizados así, una búsqueda ingenua de todos los caminos de A a B terminará perdiendo O (2 n ) tiempo examinando todos los posibles callejones sin salida antes de darse por vencido.
Por supuesto, la adición de un borde para el nodo de destino B a partir de uno de los nodos en la camarilla (distinto de C), o desde la última capa de la DAG, sería crear un exponencialmente gran número de posibles caminos de la A a B, y una El algoritmo de búsqueda puramente local no puede decir de antemano si encontrará tal ventaja o no. Por lo tanto, en cierto sentido, la baja sensibilidad de salida de estas búsquedas ingenuas se debe a su falta de conocimiento de la estructura global del gráfico.
Si bien hay varios métodos de preprocesamiento (como la eliminación iterativa de nodos de hoja, la búsqueda de separadores de vértices de un solo nodo, etc.) que podrían usarse para evitar algunos de estos "callejones sin salida de tiempo exponencial", no conozco ningún general Truco de preprocesamiento que podría eliminarlos en todos los casos. Una solución general sería verificar en cada paso de la búsqueda si el nodo de destino aún es accesible (usando una subbúsqueda) y retroceder temprano si no lo es, pero lamentablemente, eso ralentizaría significativamente la búsqueda (en el peor de los casos) , proporcionalmente al tamaño del gráfico) para muchos gráficos que no contienen esos callejones sin salida patológicos.