El algoritmo A* 1. Empezar con ABIERTO conteniendo sólo el nodo inicial. Poner el valor g de ese nodo a O, su valor h' al que corresponda, y su valor f' a h' + O, es decir h'. Inicializar CERRADOS como la lista vacía. 2. Repetir el siguiente procedimiento hasta que se encuentre el nodo meta: si no existen nodos en ABIERTOS, informar del fallo. De lo contrario, tomar aquel nodo de ABIERTOS con mejor valor f'. Llamarle MEJORNODO. Quitarlo de ABIERTOS. Colocarlo en CERRADOS. Mirar si MEJORNODO es un nodo meta. Si lo es, salir e informar de la solución (bien sea MEJORNODO si lo único que queremos es el nodo, o el camino que se ha creado entre el estado inicial y MEJORNODO si estamos interesados en el camino). En caso contrario, generar los sucesores de MEJORNODO, pero no poner MEJORNODO apuntando aún a ellos (antes debemos mirar si alguno de ellos ya ha sido generado). Para cada SUCESOR, hacer lo siguiente: 1. Poner a SUCESOR apuntando a MEJORNODO. Estos enlaces hacia atrás hacen posible recuperar el camino una vez que se ha encontrado la solución. 2. Calcular g(SUCESOR) = g(MEJORNODO) + costes de ir desde MEJORNODO hasta SUCESOR. 3. Mirar si SUCESOR está contenido en ABIERTOS (es decir, si ya ha sido generado pero no procesado). Si es así, llamemos a ese nodo VIEJO. Puesto que este nodo ya existe en el grafo, podemos desechar SUCESOR, y añadir VIEJO a la lista de los sucesores de MEJORNODO. Ahora debemos decidir si el enlace paterno de VIEJO debería apuntar a MEJORNODO. Debería ocurrir así si el camino que hemos encontrado hasta SUCESOR es de menor coste que el mejor camino actual hasta VIEJO (puesto que SUCESOR y VIEJO son en realidad el mismo nodo). Para ver si cuesta menos llegar hasta VIEJO a través de su padre actual o hasta SUCESOR a través de MEJORNODO, comparar sus valores g. Si VIEJO tiene menor (o igual) coste, no necesitamos hacer nada. Si SUCESOR tiene menor coste, entonces hacer que el enlace paterno de VIEJO apunte hacia MEJORNODO, grabar el nuevo camino óptimo en g(VIEJO), y actualizar f'(VIEJO). 4. Si SUCESOR no estaba en ABIERTOS, ver si estaba en CERRADOS. En caso afirmativo, llamar VIEJO al nodo de CERRADOS, y añadir VIEJO a la lista de los sucesores de MEJORNODO. Mirar si el nuevo camino es mejor que el viejo tal como se hizo en el paso 2.3 y actualizar apropiadamente el enlace paterno y los valores de g y f'. Si acabamos de encontrar un mejor camino a VIEJO, debemos propagar la mejora a los sucesores de VIEJO. Esto es un poco delicado. VIEJO apunta a sus sucesores. Cada sucesor a su vez apunta a sus sucesores, y así sucesivamente hasta que cada rama termina en un nodo que bien ya está en ABIERTOS o no tiene sucesores. Por tanto, para propagar el nuevo coste hacia abajo, podemos hacer una búsqueda transversal en profundidad del árbol, empezando en VIEJO, cambiando el valor g de cada nodo (y por tanto su valor f'), terminando cada rama cuando se alcanza bien un nodo sin sucesores o bien un nodo para el que ya se ha encontrado un camino equivalente o mejor.4 Es fácil examinar esta La segunda comprobación garantiza que el algoritmo acabará aunque haya ciclos en el grafo. Si hay un ciclo, la segunda vez que visitemos un nodo veremos que el camino no es mejor que la primera vez que lo visitamos, y la propagación se detendrá. 4 condición. El enlace paterno de cada nodo apunta hacia atrás a su mejor predecesor conocido. Conforme propagamos a un nodo siguiente, debemos mirar si su predecesor apunta al nodo del que estamos viniendo. Si lo hace así, debemos continuar la propagación. Si no, su valor g ya refleja el mejor camino del que forma parte. Por tanto la propagación debe parar allí. Pero es posible que al propagar el nuevo valor de g hacia abajo, el camino que estamos siguiendo pueda volverse mejor que el camino a través del antecesor actual. Por tanto debemos comparar los dos. Si el camino a través del antecesor actual es aún mejor, debemos detener la propagación. Si el camino a través del cual estamos propagando es ahora mejor, inicializar el antecesor y continuar la propagación. 5. Si SUCESOR no estaba ya en ABIERTOS o en CERRADOS, ponerlo en ABIERTOS y añadirlo a la lista de sucesores de MEJORNODO. Calcular f'(SUCESOR) = g(SUCESOR) + h'(SUCESOR). Podemos hacer algunas observaciones interesantes sobre este algoritmo. La primera concierne al papel de la función g. Nos permite escoger el nodo a expandir a continuación sobre la base, no sólo de cuán bueno parece el nodo en sí mismo (medido por h'), sino también sobre la base de cuán bueno era el camino hasta el nodo. Al incorporar g en f' no siempre elegiremos como nuestro siguiente nodo a expandir el nodo que parece más cercano a la meta. Esto es Útil si nos preocupa el camino que elijamos. Pero si sólo nos importa llegar a una solución de la forma que sea, podemos definir siempre g como 0, con lo que también elegiríamos siempre el nodo que parece más cercano a la meta. Si queremos encontrar un camino que tenga el menor número de pasos, entonces debemos poner el coste de ir desde un nodo a su sucesor como una constante, usualmente 1. Si, por otra parte, queremos encontrar el camino de menor coste y algunos operadores cuestan más que otros, entonces debemos poner el coste de ir de un nodo a otro de forma que refleje dichos costes. Así pues, el algoritmo A* puede usarse tanto si estamos interesados en encontrar un camino con el coste total mínimo, como si simplemente queremos encontrar cualquier camino de la forma más rápida posible. La segunda observación atañe a h', el estimador de h, la distancia de un nodo a la meta. Si h' es un estimador perfecto de h entonces A* convergerá. Inmediatamente a la meta sin búsqueda. Cuanto mejor sea h' más cerca estaremos de este enfoque directo. Si por otra parte, h' es siempre O, la búsqueda estará controlada por g. Si g también es O, la estrategia de búsqueda se realizará al azar. Si g es siempre 1, se realizará una búsqueda en anchura. Todos los nodos de un nivel tendrán valores g más bajos, y por tanto f' más bajos, que cualquier nodo del siguiente nivel. ¿Qué ocurre, por otra parte, si h' no es ni perfecto ni O? ¿Podemos decir algo interesante sobre el comportamiento de la búsqueda? La respuesta es afirmativa si podemos garantizar que h' nunca sobrestimará h. En ese caso, el algoritmo A* garantiza que encontraremos un camino óptimo (determinado por g) a la meta, si éste existe. Podemos ver esto fácilmente a partir de algunos ejemplos.5 Consideremos la situación mostrada en la figura 3.13. Supongamos que el coste de todos los arcos es 1. Inicialmente, todos los nodos excepto A están en ABIERTOS (aunque la figura muestra la situación dos pasos más tarde, después de haber expandido B y E). Para cada nodo, se indica f' como la suma de h' y g. En este ejemplo, el nodo B tiene la f' más baja, 4, por lo que lo expandiremos en primer lugar. Supongamos que tiene un único sucesor E, que también parece estar a tres movimientos de la meta. Ahora f'(E) es 5, igual Un algoritmo que siempre encuentre el camino óptimo a una meta, si existe, se llama admisible [Nilsson, 1980]. 5 que f'(C). Supongamos que resolvemos esta disyuntiva a favor del camino que estábamos siguiendo actualmente. Entonces expandiremos E a continuación. Supongamos que también él tiene un único sucesor F', también a tres movimientos de la meta. Es evidente que estamos haciendo movimientos sin realizar ningún progreso. Pero f'(F) = 6, que es mayor que f'(C). Por tanto expandiremos C a continuación. Así vemos que al subestimar h(B) hemos desperdiciado algún esfuerzo. Pero en un momento u otro descubrimos que B estaba más lejos de lo que pensábamos, por lo que podemos regresar y encontrar otro camino. Consideremos ahora la situación mostrada en la figura 3.14. De nuevo expandimos E en el primer paso. En el segundo paso volvemos a expandir E. En el siguiente paso expandimos F, y finalmente generamos G, para un camino de solución de longitud 4. Pero supongamos que existe un camino directo desde D a la solución, que produce un camino de longitud 2. Nunca lo encontraremos. Al sobrestimar h'(D) hacemos que D parezca tan malo que podemos encontrar alguna otra solución que en realidad sea peor (aunque parezca mejor), sin siquiera expandir D. En general, si h' puede sobrestimar h, no podemos asegurar que encontremos el camino de menor coste a la solución, a menos que expandamos el grafo entero hasta que todos los caminos sean más largos que la mejor solución. Pero como normalmente no necesitamos asegurarnos de tener la mejor solución, éste no es un asunto que nos preocupe demasiado. La tercera observación que podemos hacer sobre el algoritmo A* tiene que ver con las relaciones entre árboles y grafos. El algoritmo se estableció en su forma más general para aplicarlo a los grafos. Naturalmente, puede simplificarse para aplicarse a árboles, omitiendo el mirar si un nuevo nodo está en ABIERTOS o CERRADOS. Esto da lugar a que la generación de nodos sea más rápida, pero puede producir que se realice la misma búsqueda varias veces si los nodos están duplicados con frecuencia. Puede demostrarse que bajo ciertas condiciones, el algoritmo A* es óptimo en cuanto genera el menor número de nodos posible en el proceso de encontrar una solución al problema. Bajo otras condiciones no es óptimo. Para una discusión formal de estas condiciones, ver [Gelperin, 1977] y [Martelli, 1977].