Estoy tratando de determinar el mejor algoritmo de tiempo eficiente para realizar la tarea que se describe a continuación.
Tengo un conjunto de registros. Para este conjunto de registros tengo datos de conexión que indican cómo los pares de registros de este conjunto se conectan entre sí. Básicamente, esto representa un gráfico no dirigido, en el que los registros son los vértices y los datos de conexión los bordes.
Todos los registros del conjunto tienen información de conexión (es decir, no hay registros huérfanos; cada registro del conjunto se conecta a uno o más registros del conjunto).
Quiero elegir dos registros del conjunto y poder mostrar todas las rutas simples entre los registros elegidos. Por "rutas simples" me refiero a las rutas que no tienen registros repetidos en la ruta (es decir, solo rutas finitas).
Nota: Los dos registros elegidos siempre serán diferentes (es decir, el vértice inicial y final nunca serán el mismo; no habrá ciclos).
Por ejemplo:
Si tengo los siguientes registros: A B C D E y lo siguiente representa las conexiones: (A, B), (A, C), (B, A), (B, D), (B, E), (B, F), (C, A), (C, E), (C, F), (D, B), (E, C), (E, F), (F, B), (F, C), (F, E) [donde (A, B) significa que el registro A se conecta al registro B]
Si elijo B como mi registro inicial y E como mi registro final, me gustaría encontrar todas las rutas simples a través de las conexiones de registro que conectarían el registro B con el registro E.
Todos los caminos que conectan B con E: B-> E B-> F-> E B-> F-> C-> E B-> A-> C-> E B-> A-> C-> F-> E
Este es un ejemplo, en la práctica puedo tener conjuntos que contengan cientos de miles de registros.
fuente
Respuestas:
Parece que esto se puede lograr con una búsqueda en profundidad del gráfico. La búsqueda en profundidad encontrará todas las rutas no cíclicas entre dos nodos. Este algoritmo debe ser muy rápido y escalar a gráficos grandes (la estructura de datos del gráfico es escasa, por lo que solo usa tanta memoria como necesita).
Noté que el gráfico que especificó arriba solo tiene un borde que es direccional (B, E). ¿Fue un error tipográfico o es realmente un gráfico dirigido? Esta solución funciona independientemente. Lo siento, no pude hacerlo en C, soy un poco débil en esa área. Sin embargo, espero que puedas traducir este código Java sin demasiados problemas.
Graph.java:
Search.java:
Salida del programa:
fuente
El Diccionario en línea de Algoritmos y Estructuras de Datos del Instituto Nacional de Estándares y Tecnología (NIST) enumera este problema como " todos los caminos simples" y recomienda una búsqueda en profundidad . CLRS proporciona los algoritmos relevantes.
Aquí encontrará una técnica inteligente que utiliza redes de Petri.
fuente
Aquí está el pseudocódigo que se me ocurrió. Este no es un dialecto de pseudocódigo en particular, pero debería ser lo suficientemente simple de seguir.
Cualquiera quiere diferenciar esto.
[p] es una lista de vértices que representan la ruta actual.
[x] es una lista de rutas que cumplen los criterios
[s] es el vértice fuente
[d] es el vértice de destino
[c] es el vértice actual (argumento de la rutina PathFind)
Suponga que hay una forma eficiente de buscar los vértices adyacentes (línea 6).
fuente
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.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 delvisited
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:
Ejecutar este código en el gráfico de ejemplo dado produce el siguiente resultado:
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 (eliminandoB
de la lista de vecinos deC
) 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í:
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.
fuente
for path in find_simple_paths(graph, "A", "D"): print(" -> ".join(path))
,print
faltaban los paréntesis.Aquí hay una versión recursiva lógicamente más atractiva en comparación con el segundo piso.
Salida del programa
fuente
Solución en código C. Se basa en DFS que utiliza un mínimo de memoria.
fuente
Esto puede ser tarde, pero aquí está la misma versión C # del algoritmo DFS en Java de Casey para recorrer todas las rutas entre dos nodos usando una pila. La legibilidad es mejor con recursivo como siempre.
fuente
neighbours.Reverse()
? ¿EsList<T>.Reverse
?He resuelto un problema similar a este recientemente, en lugar de todas las soluciones, solo me interesaba la más corta.
Usé una búsqueda iterativa de 'amplitud primero' que usaba una cola de estado, cada una de las cuales contenía un registro que contenía un punto actual en el gráfico y la ruta tomada para llegar allí.
comienza con un solo registro en la cola, que tiene el nodo inicial y una ruta vacía.
Cada iteración a través del código quita el elemento del encabezado de la lista y verifica si es una solución (el nodo al que llegó es el que desea, si lo es, hemos terminado), de lo contrario, construye un nuevo elemento de cola con los nodos que se conectan al nodo actual y rutas modificadas que se basan en la ruta del nodo anterior, con el nuevo salto adjunto al final.
Ahora, podría usar algo similar, pero cuando encuentre una solución, en lugar de detenerse, agregue esa solución a su 'lista encontrada' y continúe.
Debe realizar un seguimiento de una lista de nodos visitados, de modo que nunca retroceda sobre sí mismo, de lo contrario, tendrá un bucle infinito.
si quieres un poco más de pseudocódigo, publica un comentario o algo así, y te daré más detalles.
fuente
Creo que deberías describir tu problema real detrás de esto. Digo esto porque pides algo que ahorre tiempo, ¡pero el conjunto de respuestas al problema parece crecer exponencialmente!
Por lo tanto, no esperaría un algoritmo mejor que algo exponencial.
Haría retroceder y revisar todo el gráfico. Para evitar ciclos, guarde todos los nodos visitados a lo largo del camino. Cuando regrese, desmarque el nodo.
Usando recursividad:
¿O está mal eso?
editar: Ah, y lo olvidé: debes eliminar las llamadas recursivas utilizando esa pila de nodos
fuente
El principio básico es que no necesita preocuparse por los gráficos. Este es un problema estándar conocido como problema de conectividad dinámica. Existen los siguientes tipos de métodos desde los cuales puede lograr que los nodos estén conectados o no:
Aquí está el código C que probé con una complejidad de tiempo mínima O (log * n) Eso significa que para la lista de bordes 65536, requiere 4 búsquedas y para 2 ^ 65536, requiere 5 búsquedas. Estoy compartiendo mi implementación del algoritmo: Curso de algoritmo de la Universidad de Princeton
SUGERENCIA: Puede encontrar la solución Java en el enlace compartido anteriormente con las explicaciones adecuadas.
fuente
buscar_rutas [s, t, d, k]
Esta pregunta es antigua y ya está respondida. Sin embargo, ninguno muestra quizás un algoritmo más flexible para lograr lo mismo. Así que arrojaré mi sombrero al ring.
Personalmente, encuentro
find_paths[s, t, d, k]
útil un algoritmo de la forma , donde:Usando la forma de infinito de su lenguaje de programación para
d
yk
le dará todas las rutas§.§ obviamente, si está utilizando un grafo dirigido y desea que todos los no dirigidos caminos entre
s
yt
tendrá que ejecutar este en ambos sentidos:Función auxiliar
Personalmente, me gusta la recursividad, aunque a veces puede ser difícil, de todos modos primero definamos nuestra función de ayuda:
Función principal
Con eso fuera del camino, la función principal es trivial:
Primero, observemos algunas cosas:
[]
es una lista no inicializada, reemplácela con el equivalente para el lenguaje de programación de su elecciónpaths_found
se pasa por referencia . Está claro que la función de recursividad no devuelve nada. Maneje esto apropiadamente.graph
está asumiendo alguna forma dehashed
estructura. Hay una gran cantidad de formas de implementar un gráfico. De cualquier manera,graph[vertex]
obtiene una lista de vértices adyacentes en una dirección gráfico ; ajuste en consecuencia.fuente
Aquí hay un pensamiento que se me viene a la cabeza:
fuente
Por lo que puedo decir, las soluciones dadas por Ryan Fox ( 58343 , Christian ( 58444 ) y usted mismo ( 58461 ) son tan buenas como parece. No creo que el recorrido primero en amplitud ayude en este caso, como no obtener todos los caminos. Por ejemplo, con los bordes
(A,B)
,(A,C)
,(B,C)
,(B,D)
y(C,D)
obtendrá caminosABD
yACD
, pero noABCD
.fuente
Encontré una manera de enumerar todas las rutas, incluidas las infinitas que contienen bucles.
http://blog.vjeux.com/2009/project/project-shortest-path.html
Encontrar trayectorias y ciclos atómicos
Lo que queremos hacer es encontrar todos los caminos posibles que van del punto A al punto B. Dado que hay ciclos involucrados, no puede simplemente pasar y enumerarlos todos. En cambio, tendrá que encontrar una ruta atómica que no se repita y los ciclos más pequeños posibles (no desea que su ciclo se repita).
La primera definición que tomé de una ruta atómica es una ruta que no pasa por el mismo nodo dos veces. Sin embargo, descubrí que no estaba aprovechando todas las posibilidades. Después de un poco de reflexión, descubrí que los nodos no son importantes, ¡sin embargo los bordes sí lo son! Entonces, un camino atómico es un camino que no pasa por el mismo borde dos veces.
Esta definición es útil, también funciona para ciclos: un ciclo atómico del punto A es una trayectoria atómica que va desde el punto A y termina en el punto A.
Implementación
Para obtener toda la ruta comenzando desde el punto A, vamos a recorrer el gráfico de forma recursiva desde el punto A. Mientras pasamos por un niño, vamos a hacer un vínculo niño -> padre para conocer todas las aristas que ya han cruzado. Antes de ir a ese hijo, debemos recorrer esa lista vinculada y asegurarnos de que no se haya recorrido el borde especificado.
Cuando llegamos al punto de destino, podemos almacenar el camino que encontramos.
Ocurre un problema cuando desea liberar la lista vinculada. Básicamente es un árbol encadenado en orden inverso. Una solución sería vincular dos veces esa lista y, cuando se hayan encontrado todas las rutas atómicas, liberar el árbol del punto de partida.
Pero una solución inteligente es utilizar un recuento de referencias (inspirado en Garbage Collection). Cada vez que agrega un enlace a un padre, agrega uno a su recuento de referencias. Luego, cuando llega al final de un camino, retrocede y se libera mientras el recuento de referencia es igual a 1. Si es mayor, simplemente elimine uno y pare.
Buscar el ciclo atómico de A es lo mismo que buscar la ruta atómica de A a A. Sin embargo, hay varias optimizaciones que podemos hacer. Primero, cuando llegamos al punto de destino, queremos guardar la ruta solo si la suma del costo de los bordes es negativa: solo queremos pasar por ciclos de absorción.
Como ha visto anteriormente, todo el gráfico se recorre cuando se busca una ruta atómica. En cambio, podemos limitar el área de búsqueda al componente fuertemente conectado que contiene A. Encontrar estos componentes requiere un simple recorrido del gráfico con el algoritmo de Tarjan.
Combinando caminos y ciclos atómicos
En este punto, tenemos todos los caminos atómicos que van de A a B y todos los ciclos atómicos de cada nodo, nos queda organizar todo para obtener el camino más corto. A partir de ahora vamos a estudiar cómo encontrar la mejor combinación de ciclos atómicos en una trayectoria atómica.
fuente
Como lo describen hábilmente algunos de los otros carteles, el problema en pocas palabras es el de utilizar un algoritmo de búsqueda en profundidad para buscar de forma recursiva en el gráfico todas las combinaciones de rutas entre los nodos finales comunicantes.
El algoritmo en sí comienza con el nodo de inicio que le da, examina todos sus enlaces salientes y avanza expandiendo el primer nodo hijo del árbol de búsqueda que aparece, buscando progresivamente más y más profundamente hasta que se encuentra un nodo de destino, o hasta que encuentra un nodo. que no tiene hijos.
Luego, la búsqueda retrocede y regresa al nodo más reciente que aún no ha terminado de explorar.
Me escribió en su blog sobre este tema muy muy recientemente, la publicación de un ejemplo C ++ aplicación en el proceso.
fuente
Agregando a la respuesta de Casey Watson, aquí hay otra implementación de Java. Inicializando el nodo visitado con el nodo de inicio.
fuente