Algoritmo A* para resolver el problema del laberinto (explicación y prueba del algoritmo, implementación y visualización de python)

Tabla de contenido

1. Introducción

2. Detalles específicos

1, BFS (primera búsqueda en amplitud)

2, Dijkstra (búsqueda uniforme de costos)

3. Búsqueda heurística

4. Algoritmo A*

4.1 Detalles del algoritmo

4.2 Algoritmos A y A*

4.3 Prueba del algoritmo A*

4.4 Proceso del algoritmo

3. Realización

1. Requisitos experimentales

2. Implementación del código

4. Código fuente


1. Introducción

       Cuando comencé a aprender sobre el algoritmo, ya había muchos buenos artículos introductorios en línea . Si ve el siguiente mapa de carreteras con estrellas rojas ★ y púrpura × que aparecen en el artículo :

        Entonces, este artículo tiene una alta probabilidad de referirse al tutorial del algoritmo A* de Red Blob Games (haga clic para saltar) . Este tutorial es un buen tutorial introductorio, con diagramas interactivos animados y descripciones del proceso fáciles de entender.

        Lo siguiente utiliza BFS , BFS codicioso y el algoritmo de Dijkstra como introducción para introducir gradualmente el algoritmo A* , de modo que haya una comprensión general preliminar del algoritmo A* .

        Hay tres imágenes en Red Blob Games para ilustrar la diferencia entre los algoritmos BFS , Dijkstra y A* . Si tiene un poco de conocimiento básico, sabrá la idea general después de leerlo:

        BFS (búsqueda primero en amplitud): cada dirección se expande por igual , por lo que su trayectoria de exploración es círculos concéntricos, círculo tras círculo, expandiéndose uniformemente círculo por círculo como ondas.

algoritmo BFS
algoritmo BFS

        Sabemos que cuando BFS se expande, expande todos los nodos adyacentes de un nodo a su vez , y la oportunidad de expandir cada nodo es justa , es decir, las oportunidades de expansión de cada nodo adyacente son las mismas y el orden es arbitrario. En el problema del laberinto , elija el nodo derecho, superior, izquierdo e inferior de un nodo (no tiene que ser derecho, izquierdo, inferior, arriba, abajo, izquierdo o derecho si lo desea) para agregar a la cola , y luego saque el nodo de la cola en el orden en que se colocó para continuar. Para expandir , los nodos derecho, superior, izquierdo e inferior de este nodo también se agregan a la cola. Luego, el proceso que se muestra en el laberinto es buscar un círculo con una distancia de 1 desde este nodo, y luego buscar un círculo con una distancia de 2 y un círculo con una distancia de 3 ...

        El efecto de animación específico es el siguiente:

Como se puede ver en lo anterior, BFS es un algoritmo muy útil         para problemas de laberintos y otras búsquedas de gráficos con pesos de ruta iguales . BFS debe ser capaz de buscar en todos los nodos accesibles . Es un algoritmo de búsqueda exhaustivo violento y puede encontrar la ruta más corta (siempre que todos los pesos de los bordes sean iguales).

        Algoritmo de Dijkstra (Dijkstra's Algorithm ) : se explorarán primero algunos nodos (direcciones, caminos) y, en general, se explorarán primero aquellos nodos y caminos con costos más pequeños. Debido a que el algoritmo se usa para encontrar la ruta más corta , la corrección del algoritmo requiere que la ruta más corta actualmente conocida se seleccione para la exploración cada vez, por lo que su trayectoria de exploración es aleatoria y desigual , al igual que el mapa de contorno de una cresta Igual. 

Algoritmo de Dijkstra

        La diferencia entre Dijkstra y BFS es que los pesos de borde de cada borde del gráfico son diferentes en este momento y BFS ya no es aplicable en este momento.

Todavía es un problema de laberinto . El         valor en el cuadro indica el costo (costo) desde el punto de partida hasta el cuadro (se puede entender .como distancia y costo) del color del cuadrado: el el costo de pasar a marrón ■ es 1 , el costo de pasar a verde ■ ​​es 5 , y el gris ■ representa un obstáculo impenetrable.

La imagen de la izquierda es BFS, la imagen de la derecha es Dijkstra

        En la imagen de la izquierda , debido a que el costo de cada bloque es el mismo , usando el algoritmo BFS , el algoritmo pasará directamente a través del área verde para alcanzar el punto final × ; en la imagen de la derecha, debido a que el costo de los bloques es diferente , utilizando el algoritmo de Dijkstra , el algoritmo pasará por alto el área verde para alcanzar el punto final × .

        La animación de ejecución de los dos algoritmos es la siguiente:       

La imagen de la izquierda es BFS, la imagen de la derecha es Dijkstra

        El proceso de algoritmo BFS y Dijkstra específico se presentará en detalle a continuación, aquí solo necesita saber la diferencia entre sus aplicaciones.

       Algoritmo A* : Prioriza los caminos que "parecen" más cercanos a la meta . Este algoritmo también busca constantemente el nodo "más potencial actualmente" estimado , que es un mapa de cresta irregular como el algoritmo de Dijkstra . Pero en comparación con la trayectoria caótica de la cresta del algoritmo de Dijkstra , tiene un propósito y es direccional . La dirección de expansión de la trayectoria siempre elige el lado más cercano al objetivo. En la figura a continuación, puede ver que se extiende hacia el lado de la punta , porque en el ojos del algoritmo A*, está más cerca del final.

Algoritmo A*

Es una modificación         del algoritmo de Dijkstra , optimizado para un único objetivo . El algoritmo de Dijkstra puede encontrar rutas a todas las ubicaciones. Pero en nuestro algoritmo de búsqueda de rutas, es posible que solo necesitemos encontrar una ruta a una ubicación, lo que significa que parte de la sobrecarga adicional en el algoritmo de Dijkstra es innecesaria. El algoritmo A* es un algoritmo de búsqueda de ruta de punto a punto , al igual que hacer clic en una determinada posición en el mapa pequeño en LOL, el sistema encontrará automáticamente la ruta y obtendrá una "línea blanca" que conduce a la posición de destino para representarla. es el camino más corto.

        Utilizará parte de su información heurística conocida para planificar razonablemente su propia dirección de exploración, evitando cierta ceguera de Dijkstra.

2. Detalles específicos

        Lo que debe mencionarse aquí es que el algoritmo A* discutido a continuación solo se usa como un algoritmo de búsqueda de caminos , y la discusión se limita a la búsqueda de caminos en el gráfico . Pero, de hecho, el algoritmo A* no solo es aplicable a la búsqueda de rutas , es una idea heurística , también se puede usar para resolver otros problemas como ocho problemas digitales, pero la mayoría de los problemas se pueden reducir a la teoría de grafos (o tree ) , por lo que solo necesitamos comprender la búsqueda de ruta para comprender la idea completa del algoritmo.

Este es un proceso de solución de un problema de ocho números utilizando el algoritmo A* Si consideramos cada cuadrado como un nodo del gráfico y el puntero como una arista con un peso de 1, es un árbol (un caso especial de un gráfico) con el camino más corto El proceso de solución

        Esto es como el algoritmo BFS . BFS no solo es adecuado para la búsqueda de rutas, como el agotamiento de la fuerza bruta que se usa comúnmente en el tema del algoritmo , pero cuando aprendemos este algoritmo, solo nos preocupamos por la búsqueda de la ruta, porque el agotamiento de la fuerza bruta es esencialmente una raíz El nodo desde el que se inicia el árbol de búsqueda .

         Al mismo tiempo, para facilitar la comprensión, se utilizan todos los gráficos de cuadrícula , pero de hecho, la aplicación es la misma y correcta en todos los tipos de estructuras de gráficos . Esto no es difícil de entender, porque el gráfico de cuadrícula puede eventualmente transformarse en un gráfico general con un peso de nodo+arista de 1 .

        Estamos muy familiarizados con el algoritmo BFS y Dijkstra , de hecho, no hay necesidad de hablar de eso. Nuestro enfoque es el algoritmo A* . La razón del desarrollo específico del proceso del algoritmo BFS y Dijkstra aquí es que, por un lado , esperamos que todos puedan entender claramente cómo se obtiene el algoritmo A*; por otro lado , Personalmente, me gusta demasiado el código de este sitio web. Las imágenes y las animaciones interactivas son realmente infinitas.

1, BFS (primera búsqueda en amplitud)

       La clave del pensamiento BFS es expandir continuamente la frontera .

El código Python de        BFS es el siguiente:

El código más básico de BFS

        La idea es:

        Primero cree una cola (Cola, primero en entrar, primero en salir: los nodos que se colocan primero se expanden primero) frontier , y la frontera se usa para almacenar los nodos que se expandirán, por lo que el punto de partida start★ debe colocarse en la frontera al principio ;

        Luego crea una colección (Set, los elementos de la colección están fuera de orden y no se repetirán) alcanzado , que se utiliza para almacenar los nodos que han sido visitados, que es lo que solemos llamar visitar.

        Siempre que la frontera no esté vacía, se toma un elemento actual de la frontera para la expansión . Para todos los vecinos actuales next , siempre que next no esté en alcance (es decir, no se haya alcanzado), coloque next en frontier y colóquelo en reach para marcarlo como alcanzado.

El código BFS         anterior no construye una ruta, solo nos dice cómo atravesar todos los puntos en el gráfico. Por lo tanto, debe modificarse para registrar la ruta de cómo llegamos a cada punto.

        La parte amarilla es la parte modificada:

El código BFS mejorado puede registrar información de ruta

        Aquí came_from se reemplaza con reach .

        came_from es un diccionario (diccionario, par clave-valor, una clave corresponde al valor) , came_from no solo puede expresarla misma función alcanzada (juzgando si se ha alcanzado un nodo), sino también juzgar si hay un par clave-valor cuya clave es este nodo en came_from came_from[nodo i] para registrar, de modo que cuando se encuentre el punto final, pueda seguir el nodo anterior a la secuencia para encontrar el punto de partida.

        Hay otra parte de la modificación: antes se juzgaba si estaba en reach , si no, se ponía directamente en el conjunto alcanzado ; ahora se juzga si estaba en came_from , si no, store came_from[el vecino del nodo siguiente ] como la corriente extendida actual .

         Cuando cada nodo almacena la información de donde proviene, la situación de todo el grafo es la siguiente:

         La flecha indicadora de arriba indica quién es el nodo anterior , de modo que cuando BFS encuentre el punto final, tengamos suficiente información para saber cómo llegamos al punto final y podamos reconstruir este camino. Métodos de la siguiente manera:

El método para encontrar la ruta de acuerdo con la información del nodo anterior.

         Comenzando desde el objetivo , siguiendo los nodos anteriores almacenados en came_from , retrocediendo uno por uno hasta el punto de inicio, durante el cual la ruta se coloca continuamente en la matriz ruta , porque cuanto más cerca está el nodo del punto final, antes se coloca in, por lo que la última ruta almacenada es una ruta inversa , y finalmente se invierte la ruta completa.

        El BFS anterior eventualmente atravesará todos los nodos en el gráfico , es decir, conocerá el camino más corto desde el punto de inicio a todos los nodos (siempre que los pesos en el gráfico sean iguales, de lo contrario no es el más corto). Pero, de hecho, generalmente solo necesitamos solicitar una ruta a un determinado nodo de destino , por lo que muchos procesos son innecesarios. El BFS original debe finalizar después de que se hayan atravesado todos los nodos , pero ahora solo necesitamos atravesar el nodo para detenernos, de modo que podamos terminar a tiempo .

        El proceso es el siguiente, una vez que se encuentra el nodo de destino, BFS deja de expandirse:

Después de que BFS encuentre el punto final, sin importar qué nodos en la frontera no estén expandidos en este momento, debe detenerse inmediatamente.

         El código modificado es el siguiente, solo es necesario agregar una condición de terminación oportuna:

La versión completa del código BFS, buscando la ruta más corta a un nodo de destino y finalizando en el tiempo

        Cuando se encuentra que el nodo que se está expandiendo actualmente es el objetivo de punto final× , el código finaliza.

2、Dijkstra( Búsqueda de Costo Uniforme

El BFS         discutido anteriormente solo se puede usar cuando el peso de cada ruta en el gráfico es igual. Si el peso de cada ruta en el gráfico no es exactamente el mismo , entonces BFS se puede usar nuevamente para atravesar todos los nodos, pero la ruta más corta no se puede obtener, porque el camino más corto El que se recorre primero no es necesariamente el más corto.

        El algoritmo de Dijkstra también es muy simple, comienza desde el punto de partida y continúa expandiéndose hacia el nodo con el costo total más corto hasta llegar al punto final ( siempre que el peso del borde no sea negativo ). La idea de la prueba simple es que el nodo seleccionado actualmente tiene el costo más corto, y luego el costo de llegar a este nodo a través de otros nodos debe ser mayor que el costo actual.

        Debido a que es necesario encontrar el nodo con el costo total más bajo , la cola original (cola, primero en entrar, primero en salir) debe modificarse a una cola de prioridad (cola de prioridad, los elementos tienen prioridad y el elemento con la prioridad más alta puede ser devuelto).

        Al mismo tiempo, además de registrar el nodo anterior de origen de este nodo , también es necesario registrar el costo actual de llegar a este nodo cost_to_far .

Modificaciones basadas en el código         BFS :

         Primero cree una frontera de cola de prioridad , coloque el punto de partida en ella y establezca el costo en 0 ( cuanto menor sea el valor de prioridad de PriorityQueue en python , cuanto mayor sea la prioridad , antes se eliminará. Pero tengo la impresión de que PriorityQueue 's put([prioridad, valor]) el primer parámetro es la prioridad y el segundo parámetro es el valor) .

        Luego cree came_from para registrar de dónde vino y cree cost_so_far para registrar el costo total de la ruta de cada nodo alcanzado actualmente.

        Luego continúe sacando el nodo actual con el menor costo de la frontera para expandirse hasta que finalmente llegue al final de la meta .

        El método extendido se convierte en: encontrar todos los nodos adyacentes al lado del actual y calcular el nuevo_costo del siguiente El método de cálculo consiste en sumar el costo actual costo_hasta_ahora [ actual ] del actual al gráfico de costo.costo( actual , siguiente ) de este borde adyacente . Si next es un nodo al que no se ha llegado o new_cost es menor que el nodo de la ruta más corta conocida, agregue o modifique el siguiente costo actual cost_to_far [ next ] , establezca la prioridad en el nuevo costo new_cost y agregue este par clave-valor a la extensión En la frontera de la cola de prioridad , el último nodo de registro del siguiente es el actual .

        Vale la pena mencionar que la ejecución del algoritmo de Dijkstra generalmente no requiere bordes negativos , pero el código de implementación de Dijkstra anterior puede manejar gráficos con bordes positivos y negativos , pero no puede manejar gráficos con ciclos negativos .

        La razón es que cuando se encuentra una ruta más corta durante la expansión, se agregará a la cola de prioridad. En el algoritmo general de Dijkstra, todos los nodos solo ingresarán a la cola de prioridad una vez, pero una vez que el código anterior encuentre que un nodo x que atraviesa otros nodos tiene una ruta más corta, pondrá el nodo x en la cola de prioridad, independientemente de si el el nodo está expandido Sin embargo, eso es para darle a este nodo la oportunidad de modificar la ruta más corta nuevamente. Entonces, si el gráfico tiene bordes negativos y no tiene ciclos negativos (lo que significa que hay una ruta más corta para todos los nodos), la ruta más corta también se puede encontrar usando el código anterior.

        Las representaciones son las siguientes:

        

En la imagen de arriba, el costo de         ir a la cuadrícula verde es mayor que el de la cuadrícula marrón.Se puede ver que primero se explorarán algunas cuadrículas marrones y luego se explorarán algunas cuadrículas verdes, es decir, cada vez que la cuadrícula con la distancia total más corta seleccionada, se está expandiendo.

3. Búsqueda heurística

        Los dos métodos anteriores se extienden en todas las direcciones Esto es razonable cuando nuestro objetivo es encontrar una ruta a todas las ubicaciones o múltiples ubicaciones , pero si solo necesitamos una ruta a una ubicación , tal costo de tiempo no es necesario.

        Nuestro propósito es permitir que la dirección de expansión del límite se expanda hacia la posición de destino, en lugar de expandirse ciegamente en otras direcciones. Para lograr el propósito anterior, necesitamos definir una función heurística ( función heurística ), que se utilizará para medir qué tan lejos está nuestro estado actual del final.

La distancia de Manhattan         se usa comúnmente en gráficos de cuadrícula , que se define de la siguiente manera:

         Si solo usamos como prioridad la distancia calculada por la función heurística, es decir, siempre damos prioridad a expandir los puntos más cercanos al punto final, entonces se obtendrán los siguientes resultados:        

        Se puede ver que con la ayuda de la función heurística, el punto final se encuentra más rápido, y esta es su ventaja: velocidad rápida.

        Este método de búsqueda es el algoritmo Greedy Best First Search (Greedy Best First Search)

        Sin embargo, si hay obstáculos, es posible que el uso exclusivo de los resultados de cálculo de la función heurística como prioridad no proporcione el resultado correcto. Como sigue:       

         Se puede ver que solo confiando en la prioridad calculada por la función heurística no se puede obtener la ruta más corta.Abandona la ventaja de asegurar la corrección del algoritmo Dijstra para expandir los nodos de la ruta más corta cada vez, por lo que no puede garantizar obtener la ruta más corta. .

        ¿Hay alguna manera de lograr tanto la velocidad como la corrección? Ese es el algoritmo A* que se presentará a continuación.

4. Algoritmo A*

4.1 Detalles del algoritmo

        El algoritmo de Dijkstra puede encontrar la ruta más corta muy bien, pero pierde tiempo innecesario explorando direcciones poco prometedoras; solo usando el algoritmo heurístico Greedy Best First Search (Greedy Best First Search) siempre elige la dirección más prometedora para explorar, pero es posible que no encuentre la ruta óptima .

        El algoritmo A* utiliza información de ambos métodos: la distancia real desde el origen hasta la ubicación actual y la distancia estimada desde la ubicación actual hasta el destino .

        El algoritmo A* considera integralmente la prioridad de cada nodo para expandirse a través de la siguiente fórmula :

       f(x)=g(x)+h(x)

        en:

        f(x)Es decir, Xla prioridad integral del nodo que se expandirá, que se calcula mediante sum , g(x)y h(x)todavía elegimos f(x)el nodo más pequeño que se expandirá para la expansión.

        g(x)es Xel costo de la distancia del nodo al origen.

        h(x)es el costo estimadoX del nodo desde el punto final .

        De la fórmula anterior, hereda la idea del algoritmo de Dijkstra como un todo, y siempre expande el f(x)nodo más corto, lo que puede asegurar que una vez que se busca el punto final, debe ser el camino más corto; al mismo tiempo, f(x)el cálculo también tiene en cuenta la distancia estimada desde el punto final, reduciendo o evitando expandir los nodos individuales poco prometedores, de modo que el proceso de búsqueda general tienda a una dirección prometedora.

        Cabe señalar que la h(x)selección anterior no es arbitraria, es la clave para garantizar la corrección del resultado final y la velocidad de la búsqueda. h(x)Cuanto mayor sea el valor, mayor será la velocidad de búsqueda, pero no es infinita, tiene sus propias restricciones. Si Xel costo real de la distancia desde el punto final es h^{'}(x), entonces h(x)se deben cumplir los siguientes requisitos para asegurar que se pueda encontrar la solución óptima, es decir, nunca puede ser mayor que la distancia real.

        h(x) \leq h^{'}(x) 

        Puede considerarse intuitivamente que h(x)se trata de una estimación conservadora .

4.2 Algoritmos A y A*

        Una cosa a tener en cuenta es la diferencia entre el algoritmo A y el algoritmo A*. La información que se encuentra actualmente no está claramente establecida, y las definiciones de los dos son algo vagas.Los siguientes son los dos algoritmos A*:

        La primera es pensar que el algoritmo A* es la idea anterior. h^{'}(x)Es decir h^{*}(x), h(x)el algoritmo A que satisface la siguiente fórmula es el algoritmo A*.

        h(x) \leq h^{*}(x)

        La segundah(x) es que a menudo hay muchas funciones de evaluación en el algoritmo , por ejemplo, podemos usar la distancia de Manhattan, la distancia diagonal o la distancia euclidiana, por lo que debe haber muchas funciones de evaluación h_{1}(x), h_{2}(x)etc. h_{3}(x)Si restringimos aún más el algoritmo A, es decir, si g^{*}(x)>0y h_{i}(x)\leq h^{*}(x)(es decir, h^{*}(x)mayor o igual que cualquier función de evaluación), entonces el algoritmo es el algoritmo A*. Se puede ver que A* es el algoritmo A óptimo bajo esta definición. Sin embargo, en aplicaciones prácticas, a menudo es difícil para nosotros juzgar o encontrar la función de valoración óptima, por lo que la diferencia entre el algoritmo A* y el algoritmo A no es muy importante, y el algoritmo A* se usa a menudo para expresar esta idea. .

4.3 Prueba del algoritmo A*

        Para la idea del algoritmo A* anterior, doy la siguiente idea simple de contra-evidencia, que puede ser defectuosa, pero espero que pueda ayudar a entender:

        Suponiendo que la ruta encontrada por el algoritmo A* no es la ruta más corta, al final del algoritmo A*, significa que se ha encontrado una ruta más larga desde el punto de inicio hasta el punto final . Para probar la contradicción , solo necesitamos probar que el algoritmo A* no se ejecutará sin problemas en este camino más largo .

        Sea el punto inicial sy el punto final t. Sea el camino más corto T_{1}y el camino encontrado por el algoritmo A* sea T_{2}. T_{1}Los nodos de esta ruta que T_{2}son diferentes del primer nodo de la ruta ason, seguidos de b, C, d, mi... t(algunos de estos nodos pueden T_{2}ser los mismos que los de la ruta, pero no importa, ya está un camino diferente en este momento). En el camino T_{2}, tel nodo anterior del nodo es metro. Al mismo tiempo, h^{'}(x)represente la distancia real desde el nodo Xhasta el punto final t. Como sigue:

        Supongamos que cuando el algoritmo A* se ejecuta hasta el final metro, se expandirá si no hay ningún accidente , es decir, el nodo ten este momento es el más pequeño entre todos los nodos que se expandirán, por lo que se seleccionará . Y lo que queremos probar es precisamente este "accidente", para que el algoritmo A* no elija más tarde , y no elija un camino más largo que el camino más corto al final del algoritmo.tpie)tmetrot

        Sabemos h^{'}(t)=0( la distancia real ta tsí mismo es 0), h(t)pero la distancia estimada de t a t debe ser menor que h^{'}(t), es decir h(t) \leq h^{'}(t)=0, por lo h(t)que también es 0 en este momento. por lo tanto:

f(t)=g(t)+h(t)=g(t)

        Representa la distancia real g(t)alcanzada hasta el momento , es decir, sla longitud del camino. La longitud del camino conocido es mayor que la longitud del camino, y la longitud del camino se puede expresar como , entonces:tT_{2}T_{2}T_{1}T_{1}g(a)+h^{'}(a)

        g(t) \geq g(a)+h^{'}(a)

Ja)En cambio,        la distancia estimada para allegar tdebe ser menor o igual a la distancia real apara llegar , entonces:th^{'}(a)

g(a)+h^{'}(a) \geq g(a)+h(a)

        entonces:

       g(t) \geq g(a)+h(a)

        Ahora mismo:

        g(t)+h(t) \geq g(a)+h(a)

        Eso es:

        f(t) \geq f(a)

        Entonces sabemos que el nodo a expandir en este momento tno es el valor mínimo, y tenemos nodos más pequeños apara expandir.

        Después de la expansión a, porque g(b)+h^{'}(b) \leq g(t)se puede lanzar de la misma manera f(t) \geq f(b), la próxima expansión es bel nodo. Podemos hacer una analogía con T_{1}todos los nodos restantes en el camino más corto ruta C, d, mi..., puede que desee establecer i, todos satisfacen:  g(i)+h^{'}(i) \leq g(t), y se puede deducir de la misma manera f(t) \geq f(i).

        Es decir, tel nodo pie)nunca será el más pequeño entre los nodos que se expandirán, y el nodo no se T_{1}expandirá hasta que se expandan los nodos restantes en la ruta más corta . tCuando se expande T_{1} el último no- tnodo de la ruta más corta, el nodo t se expande naturalmente y el algoritmo finaliza en este momento. Podemos saber que al final el camino que encontramos fue sexactamente el contrario al que habíamos supuesto.tT_{1}T_{2}

        Por lo tanto, si h(x)siempre es menor o igual que Xel costo desde el nodo hasta el destino, el algoritmo A* garantiza que podrá encontrar el camino más corto. Cuando h(x)el valor es más pequeño, el algoritmo atravesará más nodos, lo que conducirá a un algoritmo más lento. Si h(x)es tan grande que es exactamente igual al Xcosto real del nodo al destino, el algoritmo A* encontrará la mejor ruta y la velocidad es muy rápida. Desafortunadamente, esto no es posible en todos los escenarios. Porque antes de llegar al punto final, nos resulta difícil calcular exactamente a qué distancia estamos del punto final.

        Para la función de evaluación f(x)=g(x)+h(x), podemos encontrar las siguientes cosas interesantes:

        ① h(x)=0En ese momento f(x)=g(x), indicando que está completamente basado en la distancia más corta entre los nodos alcanzados en este momento, que es el algoritmo de Dijkstra.

        ② g(x)=0En ese momento , era el algoritmo Greedy Best First Search (Greedy Best First Search)

4.4 Proceso del algoritmo

        El siguiente es el pseudocódigo del algoritmo. En comparación con el proceso del algoritmo de Dijkstra mencionado anteriormente, solo se agrega información heurística :

        El proceso anterior es similar al proceso de Dijkstra y no se describirá aquí.

        Para la información que se busca actualmente en Internet, el proceso es básicamente similar al anterior, pero los detalles específicos y los nombres son diferentes. En términos generales, open_set es la frontera del código anterior . close_set es similar al nodo puesto en cost_so_far , pero la diferencia es que el pseudocódigo anterior puede manejar gráficos con bordes negativos y sin bucles negativos, mientras que los códigos generales no pueden. La siguiente es otra versión del proceso del algoritmo:

1.初始化open_set和close_set;
2.将起点加入open_set中,并设置优先级为0(优先级越小表示优先级越高);
3.如果open_set不为空,则从open_set中选取优先级最高的节点x:
    ①如果节点x为终点,则:
        从终点开始逐步追踪parent节点,一直到达起点,返回找到的结果路径,算法结束;
    ②如果节点x不是终点,则:
        1.将节点x从open_set中删除,并加入close_set中;
        2.遍历节点x所有的邻近节点:
            ①如果邻近节点y在close_set中,则:
                跳过,选取下一个邻近节点
            ②如果邻近节点y不在open_set中,则:
                设置节点m的parent为节点x,计算节点m的优先级,将节点m加入open_set中

        Al implementar el código, lo implementé principalmente en función del primer pseudocódigo.

3. Realización

1. Requisitos experimentales

        El problema del laberinto es un problema clásico de la psicología experimental. Puede haber varios caminos desde la entrada hasta la salida del laberinto, y este experimento requiere encontrar el camino más corto desde la entrada hasta la salida.

        La siguiente figura es un diagrama esquemático de un problema de laberinto de 4 × 4. Cada posición está representada por un punto en el sistema de coordenadas planas.Como se muestra en la figura, las coordenadas del punto de entrada y el punto de salida (1,1)son (4,4). Una línea que conecta dos puntos significa que dos ubicaciones están conectadas. Si no hay línea conectada, significa que no hay comunicación.

2. Implementación del código

        Para resolver el problema del laberinto mencionado anteriormente, mi idea es numerar cada nodo del laberinto rectangular mencionado anteriormente, de (1,1)izquierda a derecha desde el principio hasta 0, 1, 2, 3... Otra ventaja de esta numeración es que puede ser muy conveniente hasta El nodo se encuentra en qué fila y en qué columna.

        La lista de adyacencia de cada nodo registra los nodos adyacentes, porque es un borde no dirigido, por lo que un borde se registrará dos veces.

        El proceso de algoritmo específico se escribe de acuerdo con el pseudocódigo anterior.

        El siguiente es el código de implementación:

import numpy as np
from queue import PriorityQueue


class Map:  # 地图
    def __init__(self, width, height) -> None:
        # 迷宫的尺寸
        self.width = width
        self.height = height
        # 创建size x size 的点的邻接表
        self.neighbor = [[] for i in range(width*height)]

    # 添加边
    def addEdge(self, from_: int, to_: int):
        if (from_ not in range(self.width*self.height)) or (to_ not in range(self.width*self.height)):
            return 0
        self.neighbor[from_].append(to_)
        self.neighbor[to_].append(from_)
        return 1

    # 由序号获得该点在迷宫的x、y坐标
    def get_x_y(self, num: int):
        if num not in range(self.width*self.height):
            return -1, -1
        x = num % self.width
        y = num // self.width
        return x, y


class Astar:  # A*寻路算法
    def __init__(self, _map: Map, start: int, end: int) -> None:
        # 地图
        self.run_map = _map
        # 起点和终点
        self.start = start
        self.end = end
        # open集
        self.open_set = PriorityQueue()
        # cost_so_far表示到达某个节点的代价,也可相当于close集使用
        self.cost_so_far = dict()
        # 每个节点的前序节点
        self.came_from = dict()

        # 将起点放入,优先级设为0,无所谓设置多少,因为总是第一个被取出
        self.open_set.put((0, start))
        self.came_from[start] = -1
        self.cost_so_far[start] = 0

    # h函数计算,即启发式信息
    def heuristic(self, a, b):
        x1, y1 = self.run_map.get_x_y(a)
        x2, y2 = self.run_map.get_x_y(b)
        return abs(x1-x2) + abs(y1-y2)

    # 运行A*寻路算法,如果没找到路径返回0,找到返回1
    def find_way(self):
        # open表不为空
        while not self.open_set.empty():
            # 从优先队列中取出代价最短的节点作为当前遍历的节点,类型为(priority,node)
            current = self.open_set.get()
            # 找到终点
            if current[1] == self.end:
                break
            # 遍历邻接节点
            for next in self.run_map.neighbor[current[1]]:
                # 新的代价
                new_cost = self.cost_so_far[current[1]]+1
                # 没有到达过的点 或 比原本已经到达过的点的代价更小
                if (next not in self.cost_so_far) or (new_cost < self.cost_so_far[next]):
                    self.cost_so_far[next] = new_cost
                    priority = new_cost+self.heuristic(next, self.end)
                    self.open_set.put((priority, next))
                    self.came_from[next] = current[1]

        if self.end not in self.cost_so_far:
            return 0
        return 1

    def show_way(self):
        # 记录路径经过的节点
        result = []
        current = self.end
        # 不断寻找前序节点
        while self.came_from[current] != -1:
            result.append(current)
            current = self.came_from[current]
        # 加上起点
        result.append(current)
        # 翻转路径
        result.reverse()
        print(result)


# 初始化迷宫
theMap = Map(4, 4)
# 添加边
theMap.addEdge(0, 1)
theMap.addEdge(1, 2)
theMap.addEdge(2, 6)
theMap.addEdge(3, 7)
theMap.addEdge(4, 5)
theMap.addEdge(5, 6)
theMap.addEdge(6, 7)
theMap.addEdge(4, 8)
theMap.addEdge(5, 9)
theMap.addEdge(7, 11)
theMap.addEdge(8, 9)
theMap.addEdge(9, 10)
theMap.addEdge(10, 11)
theMap.addEdge(8, 12)
theMap.addEdge(10, 14)
theMap.addEdge(12, 13)
theMap.addEdge(13, 14)
theMap.addEdge(14, 15)
# A* 算法寻路
theAstar = Astar(theMap, 0, 15)
theAstar.find_way()
theAstar.show_way()

        Después de correr, se obtienen los siguientes resultados:

[0, 1, 2, 6, 7, 11, 10, 14, 15]

        Es decir, el camino en el gráfico es:

         Lo anterior es el cuerpo principal del código. Para visualizar mejor los resultados, utilizo la biblioteca matploblib de python para la visualización.

         La biblioteca matploblib generalmente se usa para visualizar gráficos de datos. Mi idea es usar su función de dibujo de círculos Circle para dibujar nodos, dibujar la función de rectángulo Rectangle para dibujar bordes y luego usar la función ion() de plt (matplotlib.pyplot) para abra interacciones y dibuje gráficos dinámicos , representando cada etapa en la búsqueda. Los detalles son los siguientes:

import numpy as np
from queue import PriorityQueue
import matplotlib.pyplot as plt
import matplotlib.patches as mpathes
import random

# 画布
fig, ax = plt.subplots()


class Map:  # 地图
    def __init__(self, width, height) -> None:
        # 迷宫的尺寸
        self.width = width
        self.height = height
        # 创建size x size 的点的邻接表
        self.neighbor = [[] for i in range(width*height)]

    def addEdge(self, from_: int, to_: int):    # 添加边
        if (from_ not in range(self.width*self.height)) or (to_ not in range(self.width*self.height)):
            return 0
        self.neighbor[from_].append(to_)
        self.neighbor[to_].append(from_)
        return 1

    def get_x_y(self, num: int):    # 由序号获得该点在迷宫的x、y坐标
        if num not in range(self.width*self.height):
            return -1, -1
        x = num % self.width
        y = num // self.width
        return x, y

    def drawCircle(self, num, color):    # 绘制圆形
        x, y = self.get_x_y(num)
        thePoint = mpathes.Circle(np.array([x+1, y+1]), 0.1, color=color)
        # 声明全局变量
        global ax
        ax.add_patch(thePoint)

    def drawEdge(self, from_, to_, color):    # 绘制边
        # 转化为(x,y)
        x1, y1 = self.get_x_y(from_)
        x2, y2 = self.get_x_y(to_)
        # 整体向右下方移动一个单位
        x1, y1 = x1+1, y1+1
        x2, y2 = x2+1, y2+1
        # 绘长方形代表边
        offset = 0.05
        global ax
        if from_-to_ == 1:  # ← 方向的边
            rect = mpathes.Rectangle(
                np.array([x2-offset, y2-offset]), 1+2*offset, 2*offset, color=color)
            ax.add_patch(rect)
        elif from_-to_ == -1:  # → 方向的边
            rect = mpathes.Rectangle(
                np.array([x1-offset, y1-offset]), 1+2*offset, 2*offset, color=color)
            ax.add_patch(rect)
        elif from_-to_ == self.width:  # ↑ 方向的边
            rect = mpathes.Rectangle(
                np.array([x2-offset, y2-offset]), 2*offset, 1+2*offset, color=color)
            ax.add_patch(rect)
        else:  # ↓ 方向的边
            rect = mpathes.Rectangle(
                np.array([x1-offset, y1-offset]), 2*offset, 1+2*offset, color=color)
            ax.add_patch(rect)

    def initMap(self):    # 绘制初始的迷宫
        # 先绘制边
        for i in range(self.width*self.height):
            for next in self.neighbor[i]:
                self.drawEdge(i, next, '#afeeee')

        # 再绘制点
        for i in range(self.width*self.height):
            self.drawCircle(i, '#87cefa')


class Astar:  # A*寻路算法
    def __init__(self, _map: Map, start: int, end: int) -> None:
        # 地图
        self.run_map = _map
        # 起点和终点
        self.start = start
        self.end = end
        # open集
        self.open_set = PriorityQueue()
        # cost_so_far表示到达某个节点的代价,也可相当于close集使用
        self.cost_so_far = dict()
        # 每个节点的前序节点
        self.came_from = dict()

        # 将起点放入,优先级设为0,无所谓设置多少,因为总是第一个被取出
        self.open_set.put((0, start))
        self.came_from[start] = -1
        self.cost_so_far[start] = 0

        # 标识起点和终点
        self.run_map.drawCircle(start, '#ff8099')
        self.run_map.drawCircle(end, '#ff4d40')

    def heuristic(self, a, b):    # h函数计算,即启发式信息
        x1, y1 = self.run_map.get_x_y(a)
        x2, y2 = self.run_map.get_x_y(b)
        return abs(x1-x2) + abs(y1-y2)

    def find_way(self):    # 运行A*寻路算法,如果没找到路径返回0,找到返回1
        while not self.open_set.empty():  # open表不为空
            # 从优先队列中取出代价最短的节点作为当前遍历的节点,类型为(priority,node)
            current = self.open_set.get()

            # 展示A*算法的执行过程
            if current[1] != self.start:
                # 当前节点的前序
                pre = self.came_from[current[1]]
                # 可视化
                self.run_map.drawEdge(pre, current[1], '#fffdd0')
                if pre != self.start:
                    self.run_map.drawCircle(pre, '#99ff4d')
                else:  # 起点不改色
                    self.run_map.drawCircle(pre, '#ff8099')
                if current[1] != self.end:
                    self.run_map.drawCircle(current[1], '#99ff4d')
                else:
                    self.run_map.drawCircle(current[1], '#ff4d40')
                # 显示当前状态
                plt.show()
                plt.pause(0.5)

            # 找到终点
            if current[1] == self.end:
                break
            # 遍历邻接节点
            for next in self.run_map.neighbor[current[1]]:
                # 新的代价
                new_cost = self.cost_so_far[current[1]]+1
                # 没有到达过的点 或 比原本已经到达过的点的代价更小
                if (next not in self.cost_so_far) or (new_cost < self.cost_so_far[next]):
                    self.cost_so_far[next] = new_cost
                    priority = new_cost+self.heuristic(next, self.end)
                    self.open_set.put((priority, next))
                    self.came_from[next] = current[1]

    def show_way(self):  # 显示最短路径
        # 记录路径经过的节点
        result = []
        current = self.end

        if current not in self.cost_so_far:
            return

        # 不断寻找前序节点
        while self.came_from[current] != -1:
            result.append(current)
            current = self.came_from[current]
        # 加上起点
        result.append(current)
        # 翻转路径
        result.reverse()
        # 生成路径
        for point in result:
            if point != self.start:  # 不是起点
                # 当前节点的前序
                pre = self.came_from[point]
                # 可视化
                self.run_map.drawEdge(pre, point, '#ff2f76')
                if pre == self.start:  # 起点颜色
                    self.run_map.drawCircle(pre, '#ff8099')
                elif point == self.end:  # 终点颜色
                    self.run_map.drawCircle(point, '#ff4d40')
                # 显示当前状态
                plt.show()
                plt.pause(0.1)

    def get_cost(self):  # 返回最短路径
        if self.end not in self.cost_so_far:
            return -1
        return self.cost_so_far[self.end]


# 初始化迷宫
theMap = Map(4, 4)

# 设置迷宫显示的一些参数
plt.xlim(0, theMap.width+1)
plt.ylim(0, theMap.height+1)
# 将x轴的位置设置在顶部
ax.xaxis.set_ticks_position('top')
# y轴反向
ax.invert_yaxis()
# 等距
plt.axis('equal')
# 不显示背景的网格线
plt.grid(False)
# 允许动态
plt.ion()
# 添加边
theMap.addEdge(0, 1)
theMap.addEdge(1, 2)
theMap.addEdge(2, 6)
theMap.addEdge(3, 7)
theMap.addEdge(4, 5)
theMap.addEdge(5, 6)
theMap.addEdge(6, 7)
theMap.addEdge(4, 8)
theMap.addEdge(5, 9)
theMap.addEdge(7, 11)
theMap.addEdge(8, 9)
theMap.addEdge(9, 10)
theMap.addEdge(10, 11)
theMap.addEdge(8, 12)
theMap.addEdge(10, 14)
theMap.addEdge(12, 13)
theMap.addEdge(13, 14)
theMap.addEdge(14, 15)

# 初始化迷宫
theMap.initMap()

# A* 算法寻路
theAstar = Astar(theMap, 0, 15)
theAstar.find_way()
theAstar.show_way()

# 输出最短路径长度
theCost = theAstar.get_cost()
if theCost == -1:
    print("不存在该路径!")
else:
    print("从起点到终点的最短路径长度为: ", theCost)

# 关闭交互,展示结果
plt.ioff()
plt.show()

        El efecto de ejecución es el siguiente:

         La salida es la siguiente:

从起点到终点的最短路径长度为:  8

         Prueba para un gráfico un poco más grande (6x6):

# 初始化迷宫
theMap = Map(6, 6)

# 设置迷宫显示的一些参数
plt.xlim(0, theMap.width+1)
plt.ylim(0, theMap.height+1)
# 将x轴的位置设置在顶部
ax.xaxis.set_ticks_position('top')
# y轴反向
ax.invert_yaxis()
# 等距
plt.axis('equal')
# 不显示背景的网格线
plt.grid(False)
# 允许动态
plt.ion()

# 添加边
theMap.addEdge(0, 1)
theMap.addEdge(1, 2)
theMap.addEdge(2, 3)
theMap.addEdge(3, 4)
theMap.addEdge(4, 5)
theMap.addEdge(1, 7)
theMap.addEdge(3, 9)
theMap.addEdge(4, 10)
theMap.addEdge(5, 11)
theMap.addEdge(6, 7)
theMap.addEdge(8, 9)
theMap.addEdge(6, 12)
theMap.addEdge(7, 13)
theMap.addEdge(8, 14)
theMap.addEdge(10, 16)
theMap.addEdge(11, 17)
theMap.addEdge(12, 13)
theMap.addEdge(13, 14)
theMap.addEdge(15, 16)
theMap.addEdge(16, 17)
theMap.addEdge(14, 20)
theMap.addEdge(15, 21)
theMap.addEdge(16, 22)
theMap.addEdge(17, 23)
theMap.addEdge(18, 19)
theMap.addEdge(19, 20)
theMap.addEdge(20, 21)
theMap.addEdge(22, 23)
theMap.addEdge(18, 24)
theMap.addEdge(19, 25)
theMap.addEdge(20, 26)
theMap.addEdge(22, 28)
theMap.addEdge(26, 27)
theMap.addEdge(27, 28)
theMap.addEdge(24, 30)
theMap.addEdge(27, 33)
theMap.addEdge(29, 35)
theMap.addEdge(30, 31)
theMap.addEdge(31, 32)
theMap.addEdge(33, 34)
theMap.addEdge(34, 35)

# 初始化迷宫
theMap.initMap()

# A* 算法寻路
theAstar = Astar(theMap, 0, 35)
theAstar.find_way()
theAstar.show_way()

# 输出最短路径长度
theCost = theAstar.get_cost()
if theCost == -1:
    print("不存在该路径!")
else:
    print("从起点到终点的最短路径长度为: ", theCost)

# 关闭交互,展示结果
plt.ioff()
plt.show()

        resultado de la operación:

        

         Resultado de salida:

从起点到终点的最短路径长度为:  10

        Se puede ver que el resultado de la operación es correcto.

        Pero descubrimos que cada vez que ingresamos un nuevo gráfico, tenemos que ingresar muchos bordes, lo que es muy inconveniente para depurar gráficos más complejos. ¿Hay alguna forma de permitir que el programa genere un laberinto aleatoriamente después de establecer el tamaño del laberinto?

        Para hacer esto, podemos escribir una función que genere aleatoriamente un laberinto.

        El método de generación aleatoria que utilicé es un método simple de búsqueda profunda. El laberinto en el estado inicial no tiene bordes, solo una matriz de nodos del tamaño especificado. Comenzando desde el punto de inicio, explore cuatro direcciones por turno (el orden de exploración en las cuatro direcciones es aleatorio), si el punto adyacente en esta dirección no ha sido explorado, entonces genere un borde y avance a este punto al mismo tiempo. Para este punto, continúe repitiendo el proceso anterior hasta que se exploren todos los puntos y el algoritmo finalice.

    # 寻找
    def search(self, current: int):
        # 四个方向的顺序
        sequence = [i for i in range(4)]
        # 打乱顺序
        random.shuffle(sequence)
        # 依次选择四个方向
        for i in sequence:
            # 要探索的位置
            x = self.direction[i]+current

            # 跨了一行
            if (current % self.width == self.width-1 and self.direction[i] == 1) or (current % self.width == 0 and self.direction[i] == -1):
                continue

            # 要探索的位置没有超出范围 且 该位置没有被探索过
            if 0 <= x < self.width*self.height and self.visited[x] == 0:
                self.addEdge(current, x)
                self.visited[x] = 1
                self.search(x)


    def randomCreateMap(self, start, k):  # 随机生成迷宫
        # 标识每个节点是否被探索过
        self.visited = np.zeros(self.width*self.height)
        self.visited[start] = 1
        # 四个方向,分别代表上、下、左、右
        self.direction = {0: -self.width,
                          1: self.width,
                          2: -1,
                          3: 1}
        # 从起点开始
        self.search(start)

        Los siguientes son laberintos de 10x10, 20x20, 30x25 generados aleatoriamente:

10x10, empieza en 0, termina en 99
20x20, empieza en 0, termina en 399

         

30x25, empieza en 0, termina en 500

         Se puede ver que el laberinto generado funciona bien y puede satisfacer las necesidades básicas. Pero debido a que el algoritmo para generar el laberinto utiliza una búsqueda profunda, solo hay un camino desde el punto de inicio hasta el punto final. Esto parece inexplicable para nosotros encontrar el camino más corto, porque una vez que se encuentra el punto final, debe ser el camino más corto. Por lo tanto, agregamos complejidad al laberinto, es decir, agregamos aleatoriamente k aristas en el laberinto, de modo que haya múltiples caminos en el gráfico.


    # 随机添加k条边
    def randomAddEdges(self, k):
        # 循环k次(可能不止k次)
        for i in range(k):
            node = random.randint(0, self.width*self.height)
            # 随机添加一个方向
            sequence = [i for i in range(4)]
            random.shuffle(sequence)
            isPick = 0
            for d in sequence:
                # 跨了一行,不存在该方向的边
                if (node % self.width == self.width-1 and self.direction[d] == 1) or (node % self.width == 0 and self.direction[d] == -1):
                    continue
                x = self.direction[d]+node
                # 该边存在
                if x in self.neighbor[node]:
                    continue
                # 该边不存在
                self.addEdge(node, x)
                isPick = 1
            # 重新添加一条边,即重新循环一次
            if isPick == 0:
                if i == 0:  # 第一次
                    i = 0
                else:
                    i -= 1

        El laberinto generado es el siguiente:

        Se puede ver que hay muchos caminos redundantes, por lo que hay más de un camino desde el punto de inicio hasta el punto final.

        Aplique el algoritmo A* a un laberinto generado aleatoriamente:

        

         La salida es la siguiente:

从起点到终点的最短路径长度为:  18

        

        La salida es la siguiente:

从起点到终点的最短路径长度为:  28

        

         La salida es la siguiente:

从起点到终点的最短路径长度为:  50

4. Código fuente

import numpy as np
from queue import PriorityQueue
import matplotlib.pyplot as plt
import matplotlib.patches as mpathes
import random

# 画布
fig, ax = plt.subplots()


class Map:  # 地图
    def __init__(self, width, height) -> None:
        # 迷宫的尺寸
        self.width = width
        self.height = height
        # 创建size x size 的点的邻接表
        self.neighbor = [[] for i in range(width*height)]

    def addEdge(self, from_: int, to_: int):    # 添加边
        if (from_ not in range(self.width*self.height)) or (to_ not in range(self.width*self.height)):
            return 0
        self.neighbor[from_].append(to_)
        self.neighbor[to_].append(from_)
        return 1

    def get_x_y(self, num: int):    # 由序号获得该点在迷宫的x、y坐标
        if num not in range(self.width*self.height):
            return -1, -1
        x = num % self.width
        y = num // self.width
        return x, y

    def drawCircle(self, num, color):    # 绘制圆形
        x, y = self.get_x_y(num)
        thePoint = mpathes.Circle(np.array([x+1, y+1]), 0.1, color=color)
        # 声明全局变量
        global ax
        ax.add_patch(thePoint)

    def drawEdge(self, from_, to_, color):    # 绘制边
        # 转化为(x,y)
        x1, y1 = self.get_x_y(from_)
        x2, y2 = self.get_x_y(to_)
        # 整体向右下方移动一个单位
        x1, y1 = x1+1, y1+1
        x2, y2 = x2+1, y2+1
        # 绘长方形代表边
        offset = 0.05
        global ax
        if from_-to_ == 1:  # ← 方向的边
            rect = mpathes.Rectangle(
                np.array([x2-offset, y2-offset]), 1+2*offset, 2*offset, color=color)
            ax.add_patch(rect)
        elif from_-to_ == -1:  # → 方向的边
            rect = mpathes.Rectangle(
                np.array([x1-offset, y1-offset]), 1+2*offset, 2*offset, color=color)
            ax.add_patch(rect)
        elif from_-to_ == self.width:  # ↑ 方向的边
            rect = mpathes.Rectangle(
                np.array([x2-offset, y2-offset]), 2*offset, 1+2*offset, color=color)
            ax.add_patch(rect)
        else:  # ↓ 方向的边
            rect = mpathes.Rectangle(
                np.array([x1-offset, y1-offset]), 2*offset, 1+2*offset, color=color)
            ax.add_patch(rect)

    def initMap(self):    # 绘制初始的迷宫
        # 先绘制边
        for i in range(self.width*self.height):
            for next in self.neighbor[i]:
                self.drawEdge(i, next, '#afeeee')

        # 再绘制点
        for i in range(self.width*self.height):
            self.drawCircle(i, '#87cefa')

    # 寻找
    def search(self, current: int):
        # 四个方向的顺序
        sequence = [i for i in range(4)]
        # 打乱顺序
        random.shuffle(sequence)
        # 依次选择四个方向
        for i in sequence:
            # 要探索的位置
            x = self.direction[i]+current

            # 跨了一行
            if (current % self.width == self.width-1 and self.direction[i] == 1) or (current % self.width == 0 and self.direction[i] == -1):
                continue

            # 要探索的位置没有超出范围 且 该位置没有被探索过
            if 0 <= x < self.width*self.height and self.visited[x] == 0:
                self.addEdge(current, x)
                self.visited[x] = 1
                self.search(x)

    # 随机添加k条边
    def randomAddEdges(self, k):
        # 循环k次(可能不止k次)
        for i in range(k):
            node = random.randint(0, self.width*self.height)
            # 随机添加一个方向
            sequence = [i for i in range(4)]
            random.shuffle(sequence)
            isPick = 0
            for d in sequence:
                # 跨了一行,不存在该方向的边
                if (node % self.width == self.width-1 and self.direction[d] == 1) or (node % self.width == 0 and self.direction[d] == -1):
                    continue
                x = self.direction[d]+node
                # 该边存在
                if x in self.neighbor[node]:
                    continue
                # 该边不存在
                self.addEdge(node, x)
                isPick = 1
            # 重新添加一条边,即重新循环一次
            if isPick == 0:
                if i == 0:  # 第一次
                    i = 0
                else:
                    i -= 1

    def randomCreateMap(self, start, k):  # 随机生成迷宫
        # 标识每个节点是否被探索过
        self.visited = np.zeros(self.width*self.height)
        self.visited[start] = 1
        # 四个方向,分别代表上、下、左、右
        self.direction = {0: -self.width,
                          1: self.width,
                          2: -1,
                          3: 1}
        # 从起点开始
        self.search(start)
        # 随机添加k条边,使得迷宫尽可能出现多条到达终点的路径
        self.randomAddEdges(k)


class Astar:  # A*寻路算法
    def __init__(self, _map: Map, start: int, end: int) -> None:
        # 地图
        self.run_map = _map
        # 起点和终点
        self.start = start
        self.end = end
        # open集
        self.open_set = PriorityQueue()
        # cost_so_far表示到达某个节点的代价,也可相当于close集使用
        self.cost_so_far = dict()
        # 每个节点的前序节点
        self.came_from = dict()

        # 将起点放入,优先级设为0,无所谓设置多少,因为总是第一个被取出
        self.open_set.put((0, start))
        self.came_from[start] = -1
        self.cost_so_far[start] = 0

        # 标识起点和终点
        self.run_map.drawCircle(start, '#ff8099')
        self.run_map.drawCircle(end, '#ff4d40')

    def heuristic(self, a, b):    # h函数计算,即启发式信息
        x1, y1 = self.run_map.get_x_y(a)
        x2, y2 = self.run_map.get_x_y(b)
        return abs(x1-x2) + abs(y1-y2)

    def find_way(self):    # 运行A*寻路算法,如果没找到路径返回0,找到返回1
        while not self.open_set.empty():  # open表不为空
            # 从优先队列中取出代价最短的节点作为当前遍历的节点,类型为(priority,node)
            current = self.open_set.get()

            # 展示A*算法的执行过程
            if current[1] != self.start:
                # 当前节点的前序
                pre = self.came_from[current[1]]
                # 可视化
                self.run_map.drawEdge(pre, current[1], '#fffdd0')
                if pre != self.start:
                    self.run_map.drawCircle(pre, '#99ff4d')
                else:  # 起点不改色
                    self.run_map.drawCircle(pre, '#ff8099')
                if current[1] != self.end:
                    self.run_map.drawCircle(current[1], '#99ff4d')
                else:
                    self.run_map.drawCircle(current[1], '#ff4d40')
                # 显示当前状态
                plt.show()
                plt.pause(0.01)

            # 找到终点
            if current[1] == self.end:
                break
            # 遍历邻接节点
            for next in self.run_map.neighbor[current[1]]:
                # 新的代价
                new_cost = self.cost_so_far[current[1]]+1
                # 没有到达过的点 或 比原本已经到达过的点的代价更小
                if (next not in self.cost_so_far) or (new_cost < self.cost_so_far[next]):
                    self.cost_so_far[next] = new_cost
                    priority = new_cost+self.heuristic(next, self.end)
                    self.open_set.put((priority, next))
                    self.came_from[next] = current[1]

    def show_way(self):  # 显示最短路径
        # 记录路径经过的节点
        result = []
        current = self.end

        if current not in self.cost_so_far:
            return

        # 不断寻找前序节点
        while self.came_from[current] != -1:
            result.append(current)
            current = self.came_from[current]
        # 加上起点
        result.append(current)
        # 翻转路径
        result.reverse()
        # 生成路径
        for point in result:
            if point != self.start:  # 不是起点
                # 当前节点的前序
                pre = self.came_from[point]
                # 可视化
                self.run_map.drawEdge(pre, point, '#ff2f76')
                if pre == self.start:  # 起点颜色
                    self.run_map.drawCircle(pre, '#ff8099')
                elif point == self.end:  # 终点颜色
                    self.run_map.drawCircle(point, '#ff4d40')
                # 显示当前状态
                plt.show()
                plt.pause(0.005)

    def get_cost(self):  # 返回最短路径
        if self.end not in self.cost_so_far:
            return -1
        return self.cost_so_far[self.end]


# 初始化迷宫,设置宽度和高度
theMap = Map(20, 20)

# 设置迷宫显示的一些参数
plt.xlim(0, theMap.width+1)
plt.ylim(0, theMap.height+1)
# 将x轴的位置设置在顶部
ax.xaxis.set_ticks_position('top')
# y轴反向
ax.invert_yaxis()
# 等距
plt.axis('equal')
# 不显示背景的网格线
plt.grid(False)
# 允许动态
plt.ion()

# 随机添加边,生成迷宫,第一个参数为起点;第二个参数为额外随机生成的边,可以表示为图的复杂程度
theMap.randomCreateMap(0, 20)

# 初始化迷宫
theMap.initMap()

# A* 算法寻路
theAstar = Astar(theMap, 0, 399)  # 设置起点和终点
theAstar.find_way()  # 寻路
theAstar.show_way()  # 显示最短路径

# 输出最短路径长度
theCost = theAstar.get_cost()
if theCost == -1:
    print("不存在该路径!")
else:
    print("从起点到终点的最短路径长度为: ", theCost)

# 关闭交互,展示结果
plt.ioff()
plt.show()

Supongo que te gusta

Origin blog.csdn.net/m0_51653200/article/details/127107592
Recomendado
Clasificación