10 luglio 2026
Algoritmo di Dijkstra: trovare il percorso minimo (e perché serve ancora nel software moderno)
Dai grafi pesati alle code di priorità: il cuore “greedy” del shortest path spiegato in modo operativo.
L’algoritmo di Dijkstra è uno dei pilastri dell’informatica applicata: routing di rete, navigazione, robotica, logistica, giochi. In questo articolo vediamo come modella il problema con grafi pesati, come funziona passo-passo, perché è un algoritmo greedy e come si implementa in pratica con una priority queue (min-heap), includendo anche la ricostruzione del percorso tramite i predecessori.
Il problema: “qual è la strada migliore da A a B?”
Molti problemi reali si riducono a una domanda semplice: come arrivo dal punto A al punto B minimizzando un costo?
Quel “costo” può essere:
- distanza (km), tempo (minuti), carburante;
- latenza in una rete;
- costo di attraversamento di una cella in una griglia;
- “fatica” o rischio in un percorso;
- qualsiasi metrica misurabile e confrontabile.
L’algoritmo di Dijkstra risolve proprio questo: trovare il percorso a costo minimo tra un nodo sorgente e gli altri nodi (o verso una destinazione specifica), in un grafo con pesi non negativi.
Il modello: grafi, nodi, archi e pesi
Per applicare Dijkstra bisogna prima trasformare il dominio in un’astrazione:
- nodi (nodes): entità (città, router, utenti, celle di una mappa…)
- archi (edges): connessioni tra entità (strade, link, relazioni…)
- pesi (weights): costo associato a ciascun arco
Quando gli archi hanno un costo, il grafo è un grafo pesato (weighted graph). Dijkstra lavora su questo tipo di struttura e cerca il cammino con somma dei pesi minima.
Nota importante: Dijkstra assume pesi non negativi. Se esistono pesi negativi, servono altre tecniche (es. Bellman–Ford).
L’idea chiave: espandere sempre il “più promettente”
Dijkstra è un algoritmo greedy: ad ogni passo sceglie ciò che sembra migliore in quel momento.
In pratica mantiene una stima della distanza minima dalla sorgente a ogni nodo e ripete questo ciclo:
- seleziona il nodo non ancora visitato con distanza stimata più piccola;
- prova a migliorare (rilassare) le distanze dei suoi vicini;
- marca il nodo come visitato.
Questa scelta “ingorda” funziona perché, con pesi non negativi, quando un nodo diventa il più vicino tra i non visitati, la sua distanza è definitiva.
Esempio rapido (concettuale)
Supponiamo di partire da A e voler raggiungere D.
Inizializzazione
dist[A] = 0dist[altri] = ∞(valore temporaneo: “non lo so ancora”)prev[*] = Noneper ricostruire il percorso- insieme
visited = ∅
Passi
- Si parte da A, si aggiornano i vicini (es. B e C).
- Poi si visita il nodo non visitato con
distpiù piccolo (es. B), si aggiornano i suoi vicini (es. D). - Si continua finché si visita D o finché si sono processati tutti i nodi raggiungibili.
Ricostruzione del percorso
Una volta calcolate le distanze, il percorso minimo non è “stampato” da solo: per ottenerlo si usa prev:
- si parte da D
- si risale
prev[D], poiprev[prev[D]], ecc. - si ottiene la lista al contrario, che va poi invertita
Questa struttura prev è uno dei dettagli più utili in produzione: senza, avresti solo il costo minimo, non la sequenza di passi.
Implementazione pratica: adjacency list + priority queue
Per grafi medi o grandi, scegliere ogni volta “il nodo con distanza minima” scorrendo una lista sarebbe troppo lento. La soluzione standard è usare una coda di priorità (in pratica un min-heap).
Rappresentare il grafo
Una forma comoda è la lista di adiacenza:
- per ogni nodo, elenco dei vicini e del peso per raggiungerli
Concettualmente:
A: { B: 2, C: 6 }
B: { A: 2, C: 9, D: 5 }
C: { A: 6, B: 9, D: 8 }
D: { B: 5, C: 8 }
Strutture dati essenziali
dist: mappa nodo → miglior distanza notaprev: mappa nodo → predecessore nel percorso minimovisited: insieme di nodi già finalizzatipq: min-heap di coppie(distanza, nodo)
Il ciclo
- estrai da
pqil nodo con distanza minore - se già visitato, salta (capita perché un nodo può entrare più volte nel heap con distanze diverse)
- per ogni vicino, calcola una
newDist = dist[current] + weight(current, neighbor) - se
newDistè migliore, aggiornadist[neighbor]eprev[neighbor], e inserisci(newDist, neighbor)nel heap
Questa combinazione (lista di adiacenza + min-heap) è il motivo per cui Dijkstra resta un’arma estremamente attuale: scala bene e si adatta a molti problemi “di percorso” nel software.
Perché questa semplicità è un vantaggio (anche nel frontend)
Anche se spesso lo associamo a GPS e routing, Dijkstra torna utile ovunque serva:
- calcolo di percorsi su griglie (editor, giochi, mappe interattive)
- suggerimento di “passi minimi” in flussi o stati (wizard, funnel, grafi di dipendenze)
- ottimizzazione di pipeline (build graph, dependency graph)
- analisi di rete (anche concettuale) per visualizzazioni e strumenti di debug
C’è un punto culturale importante: forzare la semplicità. Un algoritmo come Dijkstra funziona perché separa bene:
- modello (grafo pesato)
- regola di scelta (minimo tra i non visitati)
- aggiornamento locale (rilassamento)
- ricostruzione (predecessori)
Se il tuo codice riesce a rispecchiare queste quattro parti senza “magia”, diventa immediatamente più leggibile e testabile.
Sintesi e implicazione pratica
Dijkstra è un classico perché trasforma un problema enorme (tutti i possibili percorsi) in una procedura deterministica e incrementale:
- mantieni distanze provvisorie
- scegli sempre il nodo più vicino non ancora finalizzato
- aggiorna i vicini
- usa
prevper ricostruire il cammino
Quando devi modellare un problema di ottimizzazione “da A a B” nel tuo progetto, il primo passo non è cercare librerie: è chiederti se puoi descriverlo come un grafo pesato a pesi non negativi. Se la risposta è sì, hai già in mano una soluzione robusta, comprensibile e sorprendentemente versatile.