Algoritmul A*
A* (A-star) este unul dintre cei mai populari algoritmi de căutare informată. Combină avantajele căutării uniforme (Dijkstra) cu euristici pentru a găsi eficient calea optimă.
Concepte de bază
Funcția de evaluare
A* folosește funcția:
Unde:
- f(n) = costul total estimat al căii prin nodul n
- g(n) = costul real de la start până la nodul n
- h(n) = euristica - costul estimat de la n la scop
Euristica
O euristică h(n) este admisibilă dacă nu supraestimează niciodată costul real:
unde h^*(n) este costul real până la scop.
O euristică este consistentă (monotonă) dacă:
pentru orice succesor n' al lui n.
Algoritm
import heapq
def a_star(graf, start, scop, h):
"""
Algoritmul A*.
Args:
graf: dicționar {nod: [(vecin, cost), ...]}
start: nodul de start
scop: nodul țintă
h: funcția euristică h(nod) -> cost estimat
Returns:
(cale, cost) sau (None, inf)
"""
# Priority queue: (f, g, nod, cale)
heap = [(h(start), 0, start, [start])]
vizitat = {} # nod -> cel mai mic g cunoscut
while heap:
f, g, nod, cale = heapq.heappop(heap)
# Dacă am ajuns la scop
if nod == scop:
return cale, g
# Skip dacă am vizitat cu cost mai mic
if nod in vizitat and vizitat[nod] <= g:
continue
vizitat[nod] = g
# Explorează vecinii
for vecin, cost in graf.get(nod, []):
g_nou = g + cost
f_nou = g_nou + h(vecin)
if vecin not in vizitat or vizitat[vecin] > g_nou:
heapq.heappush(heap, (f_nou, g_nou, vecin, cale + [vecin]))
return None, float('inf')
Euristici comune
Distanța Manhattan
Pentru grile unde ne putem deplasa doar ortogonal (sus, jos, stânga, dreapta).
def manhattan(nod, scop):
return abs(nod[0] - scop[0]) + abs(nod[1] - scop[1])
Distanța Euclidiană
Pentru deplasare în orice direcție.
import math
def euclidiana(nod, scop):
return math.sqrt((nod[0] - scop[0])**2 + (nod[1] - scop[1])**2)
Distanța Chebyshev (diagonală)
Pentru grile cu mișcare diagonală permisă.
def chebyshev(nod, scop):
return max(abs(nod[0] - scop[0]), abs(nod[1] - scop[1]))
Distanța octilă
Pentru grile unde mișcarea diagonală costă \sqrt{2}.
def octila(nod, scop):
dx = abs(nod[0] - scop[0])
dy = abs(nod[1] - scop[1])
return dx + dy + (math.sqrt(2) - 2) * min(dx, dy)
Exemplu: Navigare pe grilă
import heapq
import math
def a_star_grila(grila, start, scop):
"""
A* pe o grilă 2D.
grila: matrice 2D (0 = liber, 1 = obstacol)
start, scop: tuple (linie, coloana)
"""
linii, coloane = len(grila), len(grila[0])
# Direcții: sus, jos, stânga, dreapta
directii = [(-1, 0), (1, 0), (0, -1), (0, 1)]
def h(nod):
return manhattan(nod, scop)
def vecini(nod):
l, c = nod
rezultat = []
for dl, dc in directii:
nl, nc = l + dl, c + dc
if 0 <= nl < linii and 0 <= nc < coloane and grila[nl][nc] == 0:
rezultat.append(((nl, nc), 1)) # cost 1 per pas
return rezultat
# Priority queue: (f, g, nod, cale)
heap = [(h(start), 0, start, [start])]
vizitat = {}
while heap:
f, g, nod, cale = heapq.heappop(heap)
if nod == scop:
return cale, g
if nod in vizitat:
continue
vizitat[nod] = g
for vecin, cost in vecini(nod):
if vecin not in vizitat:
g_nou = g + cost
f_nou = g_nou + h(vecin)
heapq.heappush(heap, (f_nou, g_nou, vecin, cale + [vecin]))
return None, float('inf')
# Exemplu
grila = [
[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 1, 0],
[0, 1, 0, 0, 0],
[0, 0, 0, 0, 0]
]
cale, cost = a_star_grila(grila, (0, 0), (4, 4))
print(f"Cale: {cale}")
print(f"Cost: {cost}")
Comparație cu alți algoritmi
| Algoritm | Complet | Optim | Timp | Spațiu |
|---|---|---|---|---|
| BFS | Da | Da* | O(b^d) | O(b^d) |
| DFS | Nu | Nu | O(b^m) | O(bm) |
| Dijkstra | Da | Da | O(b^d) | O(b^d) |
| A* | Da | Da** | O(b^d) | O(b^d) |
* BFS optim doar pentru costuri uniforme * A optim cu euristică admisibilă
Avantajele A*
- Optim: găsește întotdeauna calea cu cost minim (cu euristică admisibilă)
- Eficient: explorează mai puține noduri decât Dijkstra
- Flexibil: poate fi adaptat cu diferite euristici
Variante ale A*
Weighted A*
Folosește f(n) = g(n) + w \cdot h(n) cu w > 1 pentru viteză în detrimentul optimalității.
def weighted_a_star(graf, start, scop, h, w=1.5):
heap = [(w * h(start), 0, start, [start])]
vizitat = {}
while heap:
_, g, nod, cale = heapq.heappop(heap)
if nod == scop:
return cale, g
if nod in vizitat:
continue
vizitat[nod] = g
for vecin, cost in graf.get(nod, []):
if vecin not in vizitat:
g_nou = g + cost
f_nou = g_nou + w * h(vecin)
heapq.heappush(heap, (f_nou, g_nou, vecin, cale + [vecin]))
return None, float('inf')
IDA (Iterative Deepening A)
Economisește memorie prin căutare iterativă.
def ida_star(graf, start, scop, h):
"""IDA* - A* cu memorie redusă."""
def search(cale, g, threshold):
nod = cale[-1]
f = g + h(nod)
if f > threshold:
return f, None
if nod == scop:
return f, cale
minim = float('inf')
for vecin, cost in graf.get(nod, []):
if vecin not in cale:
cale.append(vecin)
t, rezultat = search(cale, g + cost, threshold)
if rezultat is not None:
return t, rezultat
minim = min(minim, t)
cale.pop()
return minim, None
threshold = h(start)
cale = [start]
while True:
t, rezultat = search(cale, 0, threshold)
if rezultat is not None:
return rezultat
if t == float('inf'):
return None
threshold = t
Exemplu: Puzzle 8
import heapq
def puzzle_8():
"""Rezolvă puzzle-ul cu 8 piese folosind A*."""
scop = ((1, 2, 3), (4, 5, 6), (7, 8, 0))
def h_manhattan(stare):
"""Suma distanțelor Manhattan pentru fiecare piesă."""
dist = 0
for i in range(3):
for j in range(3):
val = stare[i][j]
if val != 0:
goal_i, goal_j = (val - 1) // 3, (val - 1) % 3
dist += abs(i - goal_i) + abs(j - goal_j)
return dist
def gaseste_zero(stare):
for i in range(3):
for j in range(3):
if stare[i][j] == 0:
return i, j
def vecini(stare):
zi, zj = gaseste_zero(stare)
rezultat = []
for di, dj in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
ni, nj = zi + di, zj + dj
if 0 <= ni < 3 and 0 <= nj < 3:
# Creează stare nouă prin interschimbare
lista = [list(rand) for rand in stare]
lista[zi][zj], lista[ni][nj] = lista[ni][nj], lista[zi][zj]
noua_stare = tuple(tuple(rand) for rand in lista)
rezultat.append((noua_stare, 1))
return rezultat
def rezolva(start):
heap = [(h_manhattan(start), 0, start, [start])]
vizitat = set()
while heap:
f, g, stare, cale = heapq.heappop(heap)
if stare == scop:
return cale
if stare in vizitat:
continue
vizitat.add(stare)
for vecin, cost in vecini(stare):
if vecin not in vizitat:
g_nou = g + cost
f_nou = g_nou + h_manhattan(vecin)
heapq.heappush(heap, (f_nou, g_nou, vecin, cale + [vecin]))
return None
# Stare inițială
start = ((1, 2, 3), (4, 0, 6), (7, 5, 8))
solutie = rezolva(start)
print(f"Număr de pași: {len(solutie) - 1}")
for pas in solutie:
for rand in pas:
print(rand)
print()
puzzle_8()
Proprietăți importante
Eficiența euristicii
O euristică mai bună (mai aproape de costul real) duce la explorarea mai puținor noduri:
h_2 domină h_1 și va explora mai puține noduri.
Combinarea euristicilor
def h_combinat(nod, scop, euristici):
"""Combină mai multe euristici luând maximul."""
return max(h(nod, scop) for h in euristici)
Relaxare
Pentru a crea euristici admisibile, relaxăm problema:
- Puzzle 8: Permitem oricărei piese să se miște oriunde → distanța Manhattan
- Călătorie: Ignorăm obstacolele → distanța în linie dreaptă