¿Sigue siendo relevante la búsqueda de rutas precalculada?

12

Contexto

Los viejos juegos de aventuras gráficas de Lucas Arts (era ScummVM) de apuntar y hacer clic utilizaban precalculación de rutas. Aquí hay un bosquejo de la técnica.

Paso 1

El piso de cada habitación estaba dividido en lo que llamaron "cajas de paseo", que eran más o menos equivalentes a los nodos en una malla de navegación, pero limitado a las formas trapezoidales. P.ej:

 ______ _____ _________ _____
\   A  |  B  |    C    |  D   \
 \_____|     |         |_______\
       |_____|         |
             |_________|

Paso 2

Un algoritmo fuera de línea (por ejemplo, Dijkstra o A *) calcularía la ruta más corta entre cada par de nodos y almacenaría el primer paso de la ruta en una matriz 2D, indexada en cada dimensión por el nodo inicial y final utilizado. Por ejemplo, usando los cuadros de caminata de arriba:

      ___ ___ ___ ___
     | A | B | C | D | <- Start Node
  ___|___|___|___|___|
 | A | A | A | B | C |  ---
 |___|___|___|___|___|     |
 | B | B | B | B | C |     |
 |___|___|___|___|___|     |-- Next node in shortest path
 | C | B | C | C | C |     |   from Start to End
 |___|___|___|___|___|     | 
 | D | B | C | D | D |  ---
 |___|___|___|___|___| 
   ^
   |
End Node

Como puede suponer, los requisitos de memoria aumentan rápidamente a medida que aumenta el número de nodos (N ^ 2). Dado que un corto generalmente sería lo suficientemente grande como para almacenar cada entrada en la matriz, con un mapa complejo de 300 nodos que resultaría en el almacenamiento de un extra:

300^2 * sizeof(short) = 176 kilobytes

Paso 3

Por otro lado, calcular la ruta más corta entre dos nodos fue extremadamente rápido y trivial, solo una serie de búsquedas en la matriz. Algo como:

// Find shortest path from Start to End
Path = {Start}
Current = Start
WHILE Current != End
    Current = LookUp[Current, End]
    Path.Add(Current)
ENDWHILE

Aplicando este algoritmo simple para encontrar la ruta más corta de C a A devuelve:

1) Path = { C }, Current = C
2) Path = { C, B }, Current = B
3) Path = { C, B, A }, Current = A, Exit

Pregunta

Sospecho que con el potente hardware de hoy, junto con los requisitos de memoria para hacer esto en todos los niveles, cualquier beneficio que alguna vez tuvo esta técnica ahora se ve superado simplemente al realizar una A * en tiempo de ejecución.

También he oído que hoy en día las búsquedas de memoria pueden ser incluso más lentas que el cálculo general, por lo que crear tablas de búsqueda de seno y coseno ya no es tan popular.

Pero debo admitir que todavía no conozco demasiado sobre estos asuntos de eficiencia de hardware de bajo nivel, así que aprovecho esta oportunidad para pedir la opinión de aquellos más familiarizados con el tema.

En mi motor también necesitaba la capacidad de agregar y eliminar dinámicamente nodos al gráfico en tiempo de ejecución ( vea esto ), por lo que la ruta precalculada solo hizo las cosas más complicadas, así que lo descarté (sin mencionar que mi solución de tiempo de ejecución A * ya estaba funcionando perfectamente ) Aún así, me quedé preguntándome ...

En pocas palabras, ¿esta técnica sigue siendo relevante hoy en día en algún escenario?

David Gouveia
fuente
2
Creo que todavía es relevante si tienes un presupuesto de CPU ajustado. Pero una vez que desea rutas dinámicas, ya no es útil. Por cierto, miré de dónde obtuviste tu algoritmo A * y puedes optimizarlo aún más usando un minheap y algunos otros trucos. He hecho algunas iteraciones para mejorar A * en C # que puedes ver aquí: roy-t.nl/index.php/2011/09/24/… podría ser útil.
Roy T.
1
Gracias, lo he marcado como favorito y lo investigaré cuando empiece a optimizar mi aplicación. Casi utilicé la solución de Eric Lippert con algunas modificaciones menores porque era muy limpia y fácil de seguir ... Y para todos mis casos de prueba se ejecutó casi "instantáneamente", por lo que ni siquiera me molesté en optimizarla.
David Gouveia
1
Por cierto, si decides continuar con la precomputación, es posible que desees ver el algoritmo Floyd-Warshall . Construye la matriz del "siguiente paso" de manera más eficiente que usar repetidamente Dijkstra / A *.
amitp
@amitp Gracias por el consejo, ¡siempre es bueno saber sobre estas alternativas! Aunque, dado que en la mayoría de los casos la precomputación se haría fuera de línea, no habría mucho que ganar al hacerla más eficiente. A menos que seas realmente impaciente. :-)
David Gouveia
De acuerdo, aunque Floyd-Warshall también es mucho más simple de implementar que el algoritmo de Dijkstra, por lo que si aún no tiene Dijkstra implementado, vale la pena echarle un vistazo :)
amitp

Respuestas:

3

En mi motor también necesitaba la capacidad de agregar y eliminar dinámicamente nodos al gráfico en tiempo de ejecución (vea esto), por lo que la ruta precalculada solo hizo las cosas más complicadas, así que lo descarté (sin mencionar que mi solución de tiempo de ejecución A * ya estaba funcionando perfectamente ) Aún así, me quedé preguntándome ...

En pocas palabras, ¿esta técnica sigue siendo relevante hoy en día en algún escenario?

No puedo ver ningún beneficio al usar tal técnica.

Carece de la flexibilidad de un gráfico (puede tener diferentes LOD, no tienen que tener ninguna forma específica, etc.). Además, cualquier usuario de su motor sabrá qué es un gráfico y cómo usarlo. Entonces, si desean agregar una funcionalidad adicional, tendrán que aprender a implementar su extensión utilizando una situación completamente nueva para ellos.

Como mencionaste, parece que se escalaría horriblemente. También vale la pena señalar que si un gráfico encaja en el efectivo y ejecuta todos los resultados de su camino de forma consecutiva, realmente reduce el tiempo de E / S. Parece que su implementación pronto crecerá demasiado para caber en cualquier caché.

También he oído que hoy en día las búsquedas de memoria pueden ser incluso más lentas que el cálculo general, por lo que crear tablas de búsqueda de seno y coseno ya no es tan popular.

A menos que pueda ajustar todo su programa y necesite memoria en la memoria caché, va a embotellar al sacar y sacar cosas de la memoria antes de embotellar el procesador.

Sospecho que con el potente hardware de hoy, junto con los requisitos de memoria para hacer esto en todos los niveles, cualquier beneficio que alguna vez tuvo esta técnica ahora se ve superado simplemente al realizar una A * en tiempo de ejecución

También tenga en cuenta que muchos juegos tienen bucles separados para actualizar la IA. Creo que la forma en que está configurado mi proyecto es que hay un ciclo de actualización para la entrada del usuario a 60 hz, la IA es de solo 20 hz y los juegos se dibujan lo más rápido posible.

También como nota al margen, hice algo de programación GBA solo por diversión y nada se transfirió al uso de un dispositivo moderno. Para el GBA, todo se trataba de minimizar la carga de trabajo del procesador (porque era patético). También debe darse cuenta de que la mayoría de los lenguajes de alto nivel C # y Java (no tanto C ++ o C) hacen toneladas de optimizaciones para usted. En cuanto a la optimización de su código, no tiene mucho que hacer aparte de acceder a la memoria lo menos posible y cuando ejecuta tantos cálculos como sea posible antes de traer nueva memoria que la sacará del caché y se asegurará de que esté solo haciendo cosas una vez.

Editar: También para responder a su título, sí lo es. La precomputación de rutas de uso frecuente es una excelente idea y se puede hacer con A * en cualquier lugar fuera de su ciclo de juego. Por ejemplo, desde su base a un recurso en un RTS para que los recolectores no tengan que volver a calcular cada vez que quieran irse o regresar.

Trueno clásico
fuente
Acerca de su edición, en realidad no estaba hablando de la precomputación de las rutas utilizadas con frecuencia, sino estrictamente de la técnica descrita para precalcular cada ruta posible . También estoy un poco confundido acerca de cómo la mayoría de su respuesta fue contra el uso de pathfinding precalculado, pero luego, al final, dijo que sería una excelente idea. Entonces, ¿sería útil en un entorno restringido de CPU como el GBA?
David Gouveia
1
Lo malo era que intentaba señalar que la respuesta a su título fuera de contexto es sí. Si bien la respuesta relacionada con el algoritmo específico descrito en su pregunta es no. Por lo tanto, en resumen, la preprogramación de todas las rutas posibles es una mala idea, pero la precomputación de algunas rutas utilizadas con mucha frecuencia puede ser una buena idea.
ClassicThunder
2
@ClassicThunder: esta técnica de pre-computación de todas las rutas desde algunos puntos de referencia a menudo se conoce como ALT : A-star con Señales y desigualdad de triángulos : cs.princeton.edu/courses/archive/spr06/cos423/Handouts/GW05. pdf
Pieter Geerkens