Programma
Gli algoritmi di attraversamento dei grafi sono fondamentali per molte applicazioni di informatica, dallo sviluppo di videogiochi alla robotica. Questi algoritmi sono progettati per esplorare e navigare all'interno di grafi, strutture dati composte da nodi (vertici) e archi. Tra questi, l'algoritmo A* spicca per efficienza e versatilità nella ricerca di percorsi ottimali.
L'algoritmo A* è una ricerca informata, cioè utilizza una funzione euristica per guidare la ricerca verso l'obiettivo. Questa funzione stima il costo per raggiungere l'obiettivo da un dato nodo, permettendo all'algoritmo di dare priorità ai percorsi promettenti ed evitare di esplorare quelli non necessari.
In questo articolo vedremo i concetti chiave dell'algoritmo A*, la sua implementazione in Python, le sue applicazioni, oltre a vantaggi e limiti.
Per saperne di più sulla programmazione in Python, dai un'occhiata al nostro corso Introduction to Python for Developers.
Che cos'è l'algoritmo A*?
L'algoritmo A* è un potente e diffuso algoritmo di attraversamento dei grafi e ricerca del percorso. Trova il percorso più breve tra un nodo iniziale e un nodo obiettivo in un grafo pesato.

Algoritmo A*
Come funziona l'algoritmo A*?
L'algoritmo A* combina i migliori aspetti di altri due algoritmi:
- Algoritmo di Dijkstra: trova il percorso più breve verso tutti i nodi a partire da una singola sorgente.
- Greedy Best-First Search: esplora il nodo che sembra più vicino all'obiettivo, in base a una funzione euristica.
Immagina di voler trovare il percorso più breve tra due città su una mappa. Mentre l'algoritmo di Dijkstra esplorerebbe in tutte le direzioni e il Best-First Search potrebbe dirigersi dritto alla meta (rischiando di perdere scorciatoie), A* fa qualcosa di più intelligente. Considera sia:
- La distanza già percorsa dalla partenza
- Una stima intelligente della distanza rimanente fino all'obiettivo
Questa combinazione aiuta A* a prendere decisioni informate su quale percorso esplorare dopo, risultando al tempo stesso efficiente e preciso.
Componenti chiave
Per capire l'algoritmo A*, è utile conoscere questi concetti fondamentali:
- Nodi: punti del grafo (come incroci su una mappa)
- Archi: connessioni tra nodi (come strade che collegano incroci)
- Costo del percorso: il costo effettivo per muoversi da un nodo a un altro
- Euristica: una stima del costo da un qualsiasi nodo all'obiettivo
- Spazio di ricerca: l'insieme di tutti i possibili percorsi da esplorare
Nella prossima sezione approfondiremo questi concetti e vedremo come A* li usa per trovare percorsi ottimali.
Concetti chiave nella ricerca A*
L'efficienza di A* deriva dalla valutazione intelligente dei percorsi tramite tre componenti chiave: g(n), h(n) e f(n). Questi elementi lavorano insieme per guidare la ricerca verso i percorsi più promettenti.

Funzione di costo dell'algoritmo A*
Comprendere le funzioni di costo
Costo del percorso g(n)
La funzione di costo g(n) rappresenta la distanza esatta e nota dal nodo iniziale alla posizione corrente nella ricerca. A differenza dei valori stimati, questo costo è preciso e si calcola sommando tutti i pesi degli archi attraversati lungo il percorso scelto.
Matematicamente, per un percorso attraverso i nodi n0 (nodo iniziale) fino a nk (nodo corrente), possiamo esprimere g(n) come:

Dove:
- w(ni,ni+1) rappresenta il peso dell'arco che collega il nodo ni al nodo ni+1.
Man mano che ci muoviamo nel grafo, questo valore si accumula, offrendoci una misura chiara delle risorse effettive (che siano distanza, tempo o altro) spese per raggiungere la posizione corrente.
Funzione euristica h(n)
La funzione euristica h(n) fornisce una stima del costo dal nodo corrente al nodo obiettivo, fungendo da "ipotesi informata" dell'algoritmo sul percorso rimanente.
Matematicamente, per un qualsiasi nodo n, la stima euristica deve soddisfare la condizione h(n)≤h*(n), dove h*(n) è il costo reale fino all'obiettivo: è ammissibile perché non sovrastima mai il costo vero.
Nei problemi a griglia o simili a mappe, funzioni euristiche comuni includono la distanza di Manhattan e la distanza euclidea. Per le coordinate (x1,y1) del nodo corrente e (x2,y2) del nodo obiettivo, queste distanze si calcolano come segue:
Distanza di Manhattan
![]()
Distanza euclidea
![]()
Costo stimato totale f(n)
Il costo stimato totale f(n) è il fulcro del processo decisionale di A*, poiché combina sia il costo reale del percorso sia la stima euristica per valutare il potenziale di ogni nodo. Per qualsiasi nodo n, questo costo si calcola come:
![]()
Dove:
- g(n) rappresenta il costo reale dalla partenza al nodo corrente,
- h(n) rappresenta il costo stimato dal nodo corrente all'obiettivo.
L'algoritmo usa questo valore combinato per scegliere in modo strategico quale nodo esplorare successivamente, selezionando sempre dalla lista aperta il nodo con il valore di f(n) più basso, garantendo così un equilibrio ottimale tra costi noti e distanze rimanenti stimate.
Gestione delle liste di nodi
L'algoritmo A* mantiene due liste essenziali
Open list:
- Contiene i nodi da valutare
- Ordinata per valore f(n) (dal più basso)
- I nuovi nodi vengono aggiunti man mano che vengono scoperti
Closed list:
- Contiene i nodi già valutati
- Aiuta a evitare rivalutazioni
- Serve per ricostruire il percorso finale
L'algoritmo seleziona continuamente il nodo con il valore f(n) più basso dalla open list, lo valuta e lo sposta nella closed list fino a raggiungere il nodo obiettivo o determinare che non esiste alcun percorso.
Pseudocodice dell'algoritmo A*
Ora che abbiamo compreso i componenti fondamentali di A*, vediamo come si combinano in pratica. L'implementazione dell'algoritmo può essere suddivisa in passaggi chiari e logici che trasformano questi concetti in una soluzione funzionante di pathfinding.
Ecco come funziona l'algoritmo, passo dopo passo:
function A_Star(start, goal):
// Initialize open and closed lists
openList = [start] // Nodes to be evaluated
closedList = [] // Nodes already evaluated
// Initialize node properties
start.g = 0 // Cost from start to start is 0
start.h = heuristic(start, goal) // Estimate to goal
start.f = start.g + start.h // Total estimated cost
start.parent = null // For path reconstruction
while openList is not empty:
// Get node with lowest f value - implement using a priority queue
// for faster retrieval of the best node
current = node in openList with lowest f value
// Check if we've reached the goal
if current = goal:
return reconstruct_path(current)
// Move current node from open to closed list
remove current from openList
add current to closedList
// Check all neighboring nodes
for each neighbor of current:
if neighbor in closedList:
continue // Skip already evaluated nodes
// Calculate tentative g score
tentative_g = current.g + distance(current, neighbor)
if neighbor not in openList:
add neighbor to openList
else if tentative_g >= neighbor.g:
continue // This path is not better
// This path is the best so far
neighbor.parent = current
neighbor.g = tentative_g
neighbor.h = heuristic(neighbor, goal)
neighbor.f = neighbor.g + neighbor.h
return failure // No path exists
function reconstruct_path(current):
path = []
while current is not null:
add current to beginning of path
current = current.parent
return path
Analizziamo ogni componente di questa implementazione:
Fase di inizializzazione
L'algoritmo inizia predisponendo due liste essenziali:
- La open list parte con il solo nodo iniziale
- La closed list è inizialmente vuota
Ogni nodo memorizza quattro informazioni critiche:
- g: il costo reale dal nodo iniziale
- h: il costo stimato fino all'obiettivo
- f: la somma di g e h
- parent: riferimento al nodo precedente (per ricostruire il percorso)
Ciclo principale
Il cuore di A* è il suo ciclo principale, che prosegue finché:
- L'obiettivo viene raggiunto (successo)
- La open list si svuota (fallimento: nessun percorso esiste)
A ogni iterazione, l'algoritmo:
- Seleziona il nodo più promettente (f più basso) dalla open list
- Lo sposta nella closed list
- Esamina tutti i nodi vicini
Valutazione dei vicini
Per ciascun vicino, l'algoritmo:
- Salta i nodi già nella closed list
- Calcola un punteggio g provvisorio
- Aggiorna i valori del nodo se trova un percorso migliore
- Aggiunge i nuovi nodi alla open list
Ricostruzione del percorso
Una volta raggiunto l'obiettivo, l'algoritmo risale i riferimenti ai parent per costruire il percorso ottimale dalla partenza all'arrivo.
Questo approccio sistematico garantisce che A* trovi sempre il percorso ottimale se:
- La funzione euristica è ammissibile (non sovrastima mai)
- Esiste davvero un percorso tra i nodi di partenza e di arrivo
Nella prossima sezione tradurremo questo pseudocodice in una pratica implementazione in Python, con visualizzazioni che ti aiuteranno a capire come l'algoritmo esplora lo spazio di ricerca.
Implementazione in Python dell'algoritmo A*
Ora che conosciamo teoria e pseudocodice, implementiamo A* in Python. Creeremo un'implementazione pratica che potrai usare come base per i tuoi progetti. Per rendere il tutto concreto, applicheremo l'algoritmo a una griglia 2D, uno scenario comune in giochi e robotica.
Passo 1: Funzioni essenziali e import
Per prima cosa importiamo le librerie necessarie e creiamo una struttura di nodo che memorizzerà posizione e informazioni di pathfinding per ciascun punto nello spazio di ricerca.
from typing import List, Tuple, Dict, Set
import numpy as np
import heapq
from math import sqrt
def create_node(position: Tuple[int, int], g: float = float('inf'),
h: float = 0.0, parent: Dict = None) -> Dict:
"""
Create a node for the A* algorithm.
Args:
position: (x, y) coordinates of the node
g: Cost from start to this node (default: infinity)
h: Estimated cost from this node to goal (default: 0)
parent: Parent node (default: None)
Returns:
Dictionary containing node information
"""
return {
'position': position,
'g': g,
'h': h,
'f': g + h,
'parent': parent
}
Passo 2: Funzioni di supporto
Per supportare il nostro algoritmo di pathfinding, creeremo diverse funzioni di supporto. Innanzitutto, implementeremo una funzione per calcolare la distanza tra punti usando la distanza euclidea.
Poi aggiungeremo una funzione per trovare le posizioni vicine valide nella griglia, controllando accuratamente i limiti e gli ostacoli. Infine, creeremo una funzione che ci aiuta a ricostruire il percorso una volta raggiunto l'obiettivo.
def calculate_heuristic(pos1: Tuple[int, int], pos2: Tuple[int, int]) -> float:
"""
Calculate the estimated distance between two points using Euclidean distance.
"""
x1, y1 = pos1
x2, y2 = pos2
return sqrt((x2 - x1)**2 + (y2 - y1)**2)
def get_valid_neighbors(grid: np.ndarray, position: Tuple[int, int]) -> List[Tuple[int, int]]:
"""
Get all valid neighboring positions in the grid.
Args:
grid: 2D numpy array where 0 represents walkable cells and 1 represents obstacles
position: Current position (x, y)
Returns:
List of valid neighboring positions
"""
x, y = position
rows, cols = grid.shape
# All possible moves (including diagonals)
possible_moves = [
(x+1, y), (x-1, y), # Right, Left
(x, y+1), (x, y-1), # Up, Down
(x+1, y+1), (x-1, y-1), # Diagonal moves
(x+1, y-1), (x-1, y+1)
]
return [
(nx, ny) for nx, ny in possible_moves
if 0 <= nx < rows and 0 <= ny < cols # Within grid bounds
and grid[nx, ny] == 0 # Not an obstacle
]
def reconstruct_path(goal_node: Dict) -> List[Tuple[int, int]]:
"""
Reconstruct the path from goal to start by following parent pointers.
"""
path = []
current = goal_node
while current is not None:
path.append(current['position'])
current = current['parent']
return path[::-1] # Reverse to get path from start to goal
Passo 3: Implementazione principale di A*
Ora implementiamo l'algoritmo. Useremo una coda con priorità per assicurarci di esplorare sempre prima i percorsi più promettenti.
Il nostro algoritmo manterrà due insiemi: un open set per i nodi ancora da esplorare e un closed set per i nodi già verificati.
Durante l'esplorazione della griglia, aggiorneremo continuamente i costi dei percorsi quando troviamo soluzioni migliori, fino a raggiungere l'obiettivo.
def find_path(grid: np.ndarray, start: Tuple[int, int],
goal: Tuple[int, int]) -> List[Tuple[int, int]]:
"""
Find the optimal path using A* algorithm.
Args:
grid: 2D numpy array (0 = free space, 1 = obstacle)
start: Starting position (x, y)
goal: Goal position (x, y)
Returns:
List of positions representing the optimal path
"""
# Initialize start node
start_node = create_node(
position=start,
g=0,
h=calculate_heuristic(start, goal)
)
# Initialize open and closed sets
open_list = [(start_node['f'], start)] # Priority queue
open_dict = {start: start_node} # For quick node lookup
closed_set = set() # Explored nodes
while open_list:
# Get node with lowest f value
_, current_pos = heapq.heappop(open_list)
current_node = open_dict[current_pos]
# Check if we've reached the goal
if current_pos == goal:
return reconstruct_path(current_node)
closed_set.add(current_pos)
# Explore neighbors
for neighbor_pos in get_valid_neighbors(grid, current_pos):
# Skip if already explored
if neighbor_pos in closed_set:
continue
# Calculate new path cost
tentative_g = current_node['g'] + calculate_heuristic(current_pos, neighbor_pos)
# Create or update neighbor
if neighbor_pos not in open_dict:
neighbor = create_node(
position=neighbor_pos,
g=tentative_g,
h=calculate_heuristic(neighbor_pos, goal),
parent=current_node
)
heapq.heappush(open_list, (neighbor['f'], neighbor_pos))
open_dict[neighbor_pos] = neighbor
elif tentative_g < open_dict[neighbor_pos]['g']:
# Found a better path to the neighbor
neighbor = open_dict[neighbor_pos]
neighbor['g'] = tentative_g
neighbor['f'] = tentative_g + neighbor['h']
neighbor['parent'] = current_node
return [] # No path found
Passo 4: Visualizzazione
Ora creiamo una funzione di visualizzazione. Mostrerà il layout della griglia con gli eventuali ostacoli, traccerà il percorso ottimale calcolato e segnerà chiaramente le posizioni di partenza e arrivo.
import matplotlib.pyplot as plt
def visualize_path(grid: np.ndarray, path: List[Tuple[int, int]]):
"""
Visualize the grid and found path.
"""
plt.figure(figsize=(10, 10))
plt.imshow(grid, cmap='binary')
if path:
path = np.array(path)
plt.plot(path[:, 1], path[:, 0], 'b-', linewidth=3, label='Path')
plt.plot(path[0, 1], path[0, 0], 'go', markersize=15, label='Start')
plt.plot(path[-1, 1], path[-1, 0], 'ro', markersize=15, label='Goal')
plt.grid(True)
plt.legend(fontsize=12)
plt.title("A* Pathfinding Result")
plt.show()
Esempio d'uso
Ecco come usare l'implementazione:
# Create a sample grid
grid = np.zeros((20, 20)) # 20x20 grid, all free space initially
# Add some obstacles
grid[5:15, 10] = 1 # Vertical wall
grid[5, 5:15] = 1 # Horizontal wall
# Define start and goal positions
start_pos = (2, 2)
goal_pos = (18, 18)
# Find the path
path = find_path(grid, start_pos, goal_pos)
if path:
print(f"Path found with {len(path)} steps!")
visualize_path(grid, path)
else:
print("No path found!")
Output
Percorso trovato in 22 passi!

Questa implementazione è efficiente ed estensibile. Puoi modificarla facilmente per:
- Usare diverse funzioni euristiche
- Supportare diversi tipi di movimento
- Gestire griglie pesate
- Includere vincoli aggiuntivi
Nella prossima sezione vedremo alcune applicazioni pratiche di questo algoritmo e come viene usato in scenari reali.
Applicazioni dell'algoritmo di ricerca A*
L'efficienza e la flessibilità di A* lo rendono prezioso in numerosi ambiti. Ecco le aree principali in cui eccelle:
1. Videogiochi e intrattenimento
L'algoritmo A* è ampiamente usato nello sviluppo di videogiochi per le sue capacità di pathfinding ottimale. Migliora l'esperienza del giocatore consentendo movimenti dei personaggi più realistici e reattivi.
- Pathfinding dei personaggi nei giochi di strategia: A* aiuta i personaggi a trovare il percorso più breve o più sicuro verso un bersaglio, cruciale negli RTS in cui le unità devono aggirare ostacoli e nemici in modo efficace.
- Movimento degli NPC in ambienti open-world: i personaggi non giocanti (NPC) usano A* per navigare in mondi di gioco ampi e dinamici, raggiungendo obiettivi ed evitando ostacoli e altri personaggi.
- Pianificazione tattica in tempo reale in scenari di combattimento: A* viene usato per calcolare movimenti e posizionamenti efficienti delle unità durante il combattimento, assicurando che possano raggiungere rapidamente posizioni vantaggiose evitando i pericoli.
- Risoluzione di labirinti nei giochi rompicapo: A* è efficace nel navigare labirinti complessi, offrendo sfide coinvolgenti alimentando avversari che risolvono i labirinti dinamicamente o fornendo suggerimenti.
2. Sistemi di navigazione
A* è ampiamente usato nei sistemi di navigazione per ottimizzare i percorsi, tenendo conto di vari fattori come distanza e possibili ostacoli.
- Pianificazione dei percorsi nelle app GPS: A* trova il percorso più breve e veloce tra due punti, un componente essenziale del software GPS usato da milioni di persone.
- Servizi di navigazione con traffico in tempo reale: nelle app moderne, A* è combinato con dati di traffico in tempo reale per suggerire il percorso migliore, minimizzando i tempi di viaggio ed evitando le strade congestionate.
- Ottimizzazione dei percorsi con i mezzi pubblici: A* può aiutare a trovare percorsi ottimali che includano più mezzi di trasporto, garantendo coincidenze efficienti.
- Sistemi di navigazione indoor: A* è usato anche nella navigazione all'interno di edifici, come aeroporti o grandi centri commerciali, per guidare gli utenti in ambienti complessi con più livelli e ostacoli.
3. Robotica e automazione
L'algoritmo A* è cruciale in robotica, dove l'efficienza del movimento è essenziale per produttività e sicurezza.
- Pianificazione del percorso dei veicoli autonomi: le auto a guida autonoma usano A* per navigare, prendendo decisioni in tempo reale su come muoversi da A a B evitando collisioni e rispettando le regole del traffico.
- Navigazione dei robot di magazzino: nei magazzini automatizzati, i robot si affidano ad A* per muoversi in modo efficiente tra gli scaffali, riducendo i ritardi ed evitando collisioni con altri robot.
- Ottimizzazione delle traiettorie dei droni: A* aiuta i droni a pianificare traiettorie efficienti, per consegne, rilievi o uso ricreativo, evitando ostacoli e seguendo rotte ottimali.
- Pianificazione dei movimenti dei robot in produzione: in fabbrica, A* assicura che i robot possano muoversi tra le postazioni evitando collisioni con altri macchinari e mantenendo la produttività.
4. Sistemi di rete
A* è applicato anche all'ottimizzazione delle operazioni di rete, dove efficienza nel routing e uso delle risorse sono fondamentali.
- Instradamento dei pacchetti di rete: A* viene usato per determinare il percorso più efficiente per i pacchetti dati attraverso la rete, garantendo bassa latenza e alta affidabilità.
- Allocazione delle risorse nei sistemi distribuiti: A* aiuta a ottimizzare l'allocazione delle risorse, permettendo ai sistemi distribuiti di assegnare i task in modo efficiente tra i vari nodi minimizzando l'overhead.
- Progettazione dei tracciati su circuiti stampati: nella progettazione di PCB, A* può essere usato per determinare i percorsi ottimali delle piste elettriche, garantendo minima interferenza e layout efficiente.
- Ottimizzazione del cablaggio di rete: A* è impiegato nella progettazione di infrastrutture di rete fisiche, per trovare i percorsi dei cavi più efficaci al fine di ridurre i costi e massimizzare le prestazioni.
Il valore di A* sta soprattutto nella sua adattabilità tramite funzioni euristiche personalizzate, che consentono di ottimizzare per metriche diverse come distanza, tempo o consumo energetico.
Nella prossima sezione vedremo sfide comuni e tecniche di ottimizzazione per implementare A* in modo efficace.
Sfide comuni e tecniche di ottimizzazione
Pur essendo potente, per implementare A* in modo efficace bisogna affrontare alcune sfide comuni. L'ostacolo principale è la gestione efficiente delle risorse, soprattutto con spazi di ricerca molto grandi.
Le principali difficoltà includono:
- Consumo di memoria in grafi di grandi dimensioni
- Collo di bottiglia prestazionali con euristiche complesse
- Gestione dei casi di parità
- Bilanciare accuratezza e velocità di calcolo
Fortunatamente, esistono diverse strategie efficaci per affrontare queste sfide:
- Per la gestione della memoria, punta su strutture dati efficienti
- Usa heap binari per la open list invece degli array
- Implementa una tabella hash per ricerche più rapide nella closed list
- Pulisci i dati dei nodi non necessari dopo l'elaborazione
Quando le prestazioni sono critiche, considera questi miglioramenti:
- Semplifica i calcoli dell'euristica quando possibile
- Usa aritmetica intera invece della virgola mobile
- Implementa il pathfinding gerarchico per mappe grandi
Un approccio particolarmente efficace per spazi estesi è la ricerca bilaterale, cioè la ricerca simultanea dalla partenza e dall'obiettivo. Inoltre, nel pathfinding su griglia puoi migliorare sensibilmente le prestazioni precomputando i valori dell'euristica o usando tabelle di lookup.
Ricorda di scegliere le tecniche di ottimizzazione in base ai requisiti e ai vincoli specifici. La chiave è trovare il giusto equilibrio tra uso di memoria e velocità di calcolo per la tua applicazione.
Conclusione
L'algoritmo A* è uno strumento fondamentale per problemi di pathfinding e attraversamento dei grafi. In questa guida abbiamo visto i suoi concetti di base, implementato una soluzione pratica in Python ed esaminato le sue diverse applicazioni. La sua forza risiede nell'equilibrio tra accuratezza ed efficienza, che lo rende prezioso in ambiti che vanno dal gaming alla robotica.
Sebbene l'implementazione di A* presenti delle sfide, le tecniche di ottimizzazione discusse possono aiutarti a creare soluzioni efficienti. Se stai sviluppando giochi, pianificando percorsi per robot o risolvendo problemi di instradamento, comprendere l'algoritmo A* ti fornisce un potente approccio per trovare percorsi ottimali nelle tue applicazioni
Costruire algoritmi così sofisticati richiede solide basi nei concetti di programmazione Python e nelle best practice. Vuoi rafforzare le tue fondamenta in Python e affrontare algoritmi avanzati come A*?
Fai un salto di qualità con il nostro corso Intermediate Python for Developers, dove imparerai a creare funzioni personalizzate, esplorare moduli essenziali e costruire applicazioni sofisticate.
FAQ sull'algoritmo A*
Ho bisogno di conoscenze matematiche avanzate per capire l'algoritmo A*?
No, è sufficiente una comprensione di base di geometria e grafi
L'algoritmo A* garantisce di trovare il percorso più breve?
Sì, A* trova sempre il percorso ottimale se la funzione euristica non sovrastima mai il costo reale fino all'obiettivo.
Qual è la complessità temporale dell'algoritmo A*?
La complessità temporale è O(b^d), dove b è il fattore di diramazione e d è la profondità del percorso più breve.
Come si sceglie la funzione euristica giusta per A*?
L'euristica migliore dipende dal tuo problema specifico; scelte comuni includono la distanza di Manhattan per mappe a griglia e la distanza euclidea per spazi aperti.
L'algoritmo A* può gestire ostacoli dinamici o ambienti in cambiamento?
Sì, A* può essere modificato per ambienti dinamici, anche se potrebbe dover ricalcolare i percorsi quando si verificano cambiamenti.
Sono una content writer specializzata in data science. Amo creare contenuti su temi di IA/ML/DS. Esploro anche nuovi strumenti di IA e ne scrivo.

