Programación · Algoritmos
Algoritmo de búsqueda A* explicado: cómo funciona, código y complejidad
Respuesta corta
La búsqueda A* encuentra un camino más corto desde un nodo inicial hasta un nodo meta combinando el coste acumulado, g(n), con una estimación del coste restante, h(n). Siempre expande el nodo con menor f(n) = g(n) + h(n). Si la heurística nunca sobrestima el coste restante real, A* garantiza devolver un camino óptimo, y una buena heurística le permite explorar muchos menos nodos que el algoritmo de Dijkstra.
Qué problema resuelve A*
El algoritmo de Dijkstra expande los nodos en anillos de distancia creciente en todas las direcciones, incluso en las que se alejan de la meta. Cuando sabes dónde está la meta, eso es trabajo desperdiciado. A* añade una heurística, una estimación de la distancia que falta, de modo que la búsqueda se ve atraída hacia la meta. Es el algoritmo estándar para la búsqueda de caminos en videojuegos, robótica y mapas.
A* fue descrito por Peter Hart, Nils Nilsson y Bertram Raphael en 1968 en "A Formal Basis for the Heuristic Determination of Minimum Cost Paths".
La idea en palabras sencillas
| Símbolo | Significado |
|---|---|
| g(n) | Coste del mejor camino conocido desde el inicio hasta n. |
| h(n) | Estimación heurística del coste desde n hasta la meta. |
| f(n) = g(n) + h(n) | Coste estimado del camino completo más barato que pasa por n. El conjunto abierto se ordena por f. |
- Pon el nodo inicial en una cola de prioridad (el conjunto abierto) con g = 0 y f = h(inicio).
- Extrae el nodo con menor f. Si es la meta, reconstruye el camino y termina.
- Para cada vecino, calcula un g provisional = g(actual) + coste de la arista. Si mejora el g conocido del vecino, regístralo, asigna el padre e inserta el vecino con f = g + h.
- Si el conjunto abierto queda vacío, no hay camino.
Con h(n) = 0 para todos los nodos, A* se convierte exactamente en el algoritmo de Dijkstra. Con una heurística perfecta, avanza en línea recta por un camino óptimo.
Elegir una heurística
Una heurística es admisible si nunca sobrestima el coste restante real. Es consistente (o monótona) si h(n) <= cost(n, m) + h(m) para toda arista n → m. Las heurísticas consistentes también son admisibles y, con ellas, cada nodo solo necesita expandirse una vez.
| Movimiento | Heurística | Fórmula |
|---|---|---|
| 4 direcciones en una cuadrícula, coste 1 | Distancia Manhattan | abs(dx) + abs(dy) |
| 8 direcciones, diagonal con coste 1 | Distancia de Chebyshev | max(abs(dx), abs(dy)) |
| 8 direcciones, diagonal con coste √2 | Distancia octil | max(dx, dy) + (sqrt(2) - 1) * min(dx, dy) con dx, dy en valor absoluto |
| Cualquier dirección, líneas rectas | Distancia euclidiana | sqrt(dx * dx + dy * dy) |
Implementación en Python sobre una cuadrícula
La cuadrícula siguiente tiene 5 filas y 7 columnas; # marca una pared en la columna 5. Las celdas son (row, column). Los movimientos son arriba, abajo, izquierda y derecha con coste 1, así que la distancia Manhattan es admisible y consistente.
import heapq
def a_star(grid, start, goal):
rows, cols = len(grid), len(grid[0])
h = lambda p: abs(p[0] - goal[0]) + abs(p[1] - goal[1])
g = {start: 0}
parent = {start: None}
open_heap = [(h(start), start)]
while open_heap:
f, cur = heapq.heappop(open_heap)
if cur == goal:
path = []
while cur is not None:
path.append(cur)
cur = parent[cur]
return path[::-1]
if f > g[cur] + h(cur):
continue # outdated entry
r, c = cur
for nb in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nb[0] < rows and 0 <= nb[1] < cols and grid[nb[0]][nb[1]] != "#":
ng = g[cur] + 1
if ng < g.get(nb, float("inf")):
g[nb] = ng
parent[nb] = cur
heapq.heappush(open_heap, (ng + h(nb), nb))
return None
grid = [".......",
".....#.",
".....#.",
".....#.",
"......."]
path = a_star(grid, (2, 3), (2, 6))
print(len(path) - 1, path)
# 7 [(2, 3), (2, 4), (1, 4), (0, 4), (0, 5), (0, 6), (1, 6), (2, 6)]Ejemplo resuelto, paso a paso
En la cuadrícula anterior, el inicio (2, 3) está a 3 pasos de la meta (2, 6) en línea recta, así que el primer f es 0 + 3 = 3. La pared bloquea esa línea y el camino más corto real tiene 7 pasos. A* lo descubre por etapas: expande todas las celdas con el menor f y, cuando se agotan, las del siguiente f más pequeño.
| Valor de f | Celdas expandidas (en orden) | Qué ocurrió |
|---|---|---|
| 3 | (2, 3), (2, 4) | Recto hacia la meta hasta la pared. |
| 5 | (1, 3), (1, 4), (2, 2), (3, 3), (3, 4) | Un paso lateral o hacia atrás cuesta 2 más; sigue bloqueado. |
| 7 | (0, 3), (0, 4), (0, 5), (0, 6), (1, 2), (1, 6), (2, 1), (2, 6) | Rodeando la pared por arriba; la meta sale de la cola con f = 7. |
A* expandió 15 de las 32 celdas abiertas. El algoritmo de Dijkstra (A* con h = 0) expande las 32, porque cada celda abierta está como mucho a 7 pasos del inicio. En mapas grandes y abiertos el ahorro es mucho mayor.
Complejidad temporal y espacial
| Medida | Coste | Motivo |
|---|---|---|
| Tiempo en el peor caso | O((V + E) log V) | Igual que Dijkstra: una heurística inútil como h = 0 no aporta ninguna guía. |
| Tiempo típico | mucho menos que Dijkstra | Depende por completo de la calidad de la heurística; una h perfecta expande solo los nodos del camino. |
| Espacio | O(V) | El montículo abierto, los valores g y los padres. La memoria suele ser el límite real en mapas enormes. |
Entre las variantes que ahorran memoria está IDA* (A* con profundización iterativa), que guarda solo el camino actual a costa de revisitar nodos.
Cuándo usar A* y usos en el mundo real
- Usa A* cuando hay una sola meta y una estimación barata y admisible del coste restante, como la distancia geométrica en un mapa.
- Usa Dijkstra cuando necesites distancias a muchos o a todos los nodos, o cuando no exista una heurística útil. Usa BFS en grafos sin pesos, donde una heurística aporta poco beneficio.
- Videojuegos: movimiento de unidades en mapas de casillas y mallas de navegación.
- Robótica y planificación de rutas, a menudo combinadas con puntos de referencia o jerarquías precalculados.
- Resolución de rompecabezas: el 8-puzzle y el 15-puzzle con la distancia Manhattan de las fichas como heurística.
Preguntas frecuentes
¿A* siempre es óptimo?
A* devuelve un camino óptimo cuando su heurística es admisible, es decir, cuando nunca sobrestima el coste restante. Con una heurística que sobrestima puede devolver un camino más largo.
¿Cuál es la diferencia entre A* y el algoritmo de Dijkstra?
Dijkstra ordena los nodos por el coste acumulado, g(n). A* los ordena por g(n) + h(n), donde h estima el coste restante hasta la meta. A* con h = 0 es Dijkstra.
¿Qué heurística debo usar en una cuadrícula?
Usa la distancia Manhattan para movimiento en 4 direcciones, la distancia de Chebyshev o la octil para 8 direcciones según el coste de la diagonal, y la distancia euclidiana para movimiento libre.
¿Qué es la búsqueda voraz primero el mejor?
Ordena los nodos solo por h(n) e ignora el coste acumulado. A menudo es rápida, pero no garantiza un camino más corto.