0Pricing
DSA Interview Prep · Lezione

Algoritmo di Dijkstra con coda di priorità

Implementi Dijkstra usando heapq, segua i passaggi di rilassamento su un grafo pesato e risolva il problema cheapest-flights-within-k-stops.

Algoritmo di Dijkstra con coda di priorità è una lezione DSA Interview Prep gratuita su CoddyKit. Questa è la lezione 1 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DSA Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DSA Interview Prep include 4 lezioni in totale.

Cammino minimo nei grafi pesati

L'algoritmo di Dijkstra trova il cammino minimo da un singolo nodo sorgente a tutti gli altri nodi di un grafo pesato con pesi degli archi non negativi. Funziona elaborando avidamente i nodi in ordine di distanza attualmente migliore: espande sempre il nodo non visitato più vicino. La struttura dati fondamentale è un min-heap (coda con priorità), che recupera in modo efficiente il nodo con la distanza più piccola.

Panoramica dei passaggi dell'algoritmo

Algoritmo di Dijkstra: (1) Inizializzi dist[source] = 0 e dist[all others] = inf. (2) Inserisca (0, source) in un min-heap. (3) Estragga il nodo u con la distanza più piccola. Se è già stato visitato con una distanza minore, lo ignori. (4) Per ogni vicino v di u: se dist[u] + weight(u,v) < dist[v], aggiorni dist[v] e inserisca (dist[v], v) nell'heap. (5) Ripeta finché l'heap non è vuoto.

Implementazione Python con heapq

Il modulo Python heapq implementa un min-heap. Rappresentiamo il grafo come una lista di adiacenza: graph[u] = [(v, weight), ...]. L'heap memorizza tuple (distance, node). Utilizziamo un set visited per ignorare le voci obsolete dell'heap, cioè quelle inserite prima che venisse trovato un cammino migliore.

import heapq

def dijkstra(graph, source):
    n = len(graph)
    dist = [float('inf')] * n
    dist[source] = 0
    heap = [(0, source)]  # (distance, node)
    visited = set()
    
    while heap:
        d, u = heapq.heappop(heap)
        if u in visited:
            continue
        visited.add(u)
        
        for v, weight in graph[u]:
            if dist[u] + weight < dist[v]:
                dist[v] = dist[u] + weight
                heapq.heappush(heap, (dist[v], v))
    
    return dist

Esempio svolto

Consideri un grafo con 5 nodi e gli archi: 0→1 (4), 0→2 (1), 2→1 (2), 1→3 (1), 2→3 (5), 3→4 (3). I cammini minimi dal nodo 0 sono: verso 1 passando per 0→2→1, con costo 3; verso 2, con costo 1; verso 3 passando per 0→2→1→3, con costo 4; verso 4 passando per 0→2→1→3→4, con costo 7. Dijkstra trova tutti questi cammini in un'unica esecuzione, non solo il cammino verso una singola destinazione.

import heapq

def dijkstra(graph, source):
    dist = [float('inf')] * len(graph)
    dist[source] = 0
    heap = [(0, source)]
    visited = set()
    while heap:
        d, u = heapq.heappop(heap)
        if u in visited:
            continue
        visited.add(u)
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(heap, (dist[v], v))
    return dist

graph = [
    [(1,4),(2,1)],  # 0
    [(3,1)],         # 1
    [(1,2),(3,5)],   # 2
    [(4,3)],         # 3
    []               # 4
]
print(dijkstra(graph, 0))  # [0, 3, 1, 4, 7]

Perché Dijkstra non funziona con i pesi negativi

La correttezza di Dijkstra si basa sul fatto che, una volta estratto un nodo dal min-heap, la sua distanza è definitiva. Questo vale solo se i pesi degli archi sono non negativi. Con un arco negativo u→v di peso -5, dopo aver visitato v potremmo trovare un cammino che passa per u e che è più breve, ma v è già contrassegnato come visitato. Un singolo arco negativo può invalidare tutti i calcoli delle distanze successivi.

Voli più economici con al massimo K scali (LeetCode 787)

Questo problema aggiunge un vincolo: al massimo k scali. Dijkstra standard non gestisce nativamente il conteggio dei passaggi. Soluzione: estendere lo stato a (cost, node, stops_remaining). Si può usare Dijkstra con questa 3-upla, oppure Bellman-Ford con k+1 passaggi di rilassamento. La versione modificata di Dijkstra si arresta quando stops_remaining raggiunge 0, impedendo ulteriori passaggi.

import heapq
from collections import defaultdict

def findCheapestPrice(n, flights, src, dst, k):
    graph = defaultdict(list)
    for u, v, w in flights:
        graph[u].append((v, w))
    
    heap = [(0, src, k + 1)]  # (cost, node, hops_left)
    visited = {}  # node -> min hops_left seen at this cost level
    
    while heap:
        cost, node, hops = heapq.heappop(heap)
        if node == dst:
            return cost
        if hops == 0:
            continue
        if visited.get(node, 0) >= hops:
            continue
        visited[node] = hops
        for nxt, w in graph[node]:
            heapq.heappush(heap, (cost + w, nxt, hops - 1))
    return -1

print(findCheapestPrice(4,[[0,1,100],[1,2,100],[0,2,500]],0,2,1))  # 200

Analisi della complessità temporale

Con un heap binario, Dijkstra richiede O((V + E) log V) di tempo: ogni vertice viene estratto una volta (V estrazioni), ogni arco può causare un inserimento (E inserimenti) e ogni operazione sull'heap costa O(log V). Con un heap di Fibonacci, il limite migliora a O(E + V log V), ma heapq di Python è un heap binario. Per i grafi sparsi (E ≈ V), la versione con heap binario è O(V log V); per i grafi densi (E ≈ V²) è O(V² log V).

Ricostruzione del cammino minimo

Per recuperare il cammino effettivo, non solo le distanze, si mantenga un array prev: quando si aggiorna dist[v], si imposti prev[v] = u. Al termine dell'algoritmo, si ricostruisca il cammino dalla sorgente alla destinazione procedendo a ritroso: si inizi da dst, si seguano i puntatori prev fino a source e si inverta il risultato.

import heapq

def dijkstra_path(graph, source, target):
    n = len(graph)
    dist = [float('inf')] * n
    prev = [-1] * n
    dist[source] = 0
    heap = [(0, source)]
    visited = set()
    while heap:
        d, u = heapq.heappop(heap)
        if u in visited: continue
        visited.add(u)
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                prev[v] = u
                heapq.heappush(heap, (dist[v], v))
    # Reconstruct
    path, node = [], target
    while node != -1:
        path.append(node)
        node = prev[node]
    return dist[target], path[::-1]

Uso di un dict per i grafi sparsi

Quando i nodi sono stringhe o interi non contigui, si usi un defaultdict(list) per la lista di adiacenza e un normale dict per le distanze. Questo è comune nei problemi di LeetCode, come Network Delay Time, in cui i nodi sono etichettati da 1 a n. Si ricordi di usare dist = {node: inf for node in all_nodes} e di controllare i nodi irraggiungibili dopo l'algoritmo.

import heapq
from collections import defaultdict

def networkDelayTime(times, n, k):
    graph = defaultdict(list)
    for u, v, w in times:
        graph[u].append((v, w))
    
    dist = {i: float('inf') for i in range(1, n+1)}
    dist[k] = 0
    heap = [(0, k)]
    
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]: continue
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(heap, (dist[v], v))
    
    ans = max(dist.values())
    return ans if ans < float('inf') else -1

print(networkDelayTime([[2,1,1],[2,3,1],[3,4,1]], 4, 2))  # 2

Confronto con BFS per i grafi non pesati

Per i grafi non pesati, BFS trova i cammini minimi in O(V + E), più rapidamente rispetto a Dijkstra, che richiede O((V+E) log V). Dijkstra generalizza BFS ai grafi pesati usando una coda con priorità invece di una normale coda FIFO. Quando tutti i pesi degli archi sono uguali, Dijkstra si riduce a BFS. Si scelga BFS per i grafi non pesati, Dijkstra per i pesi non negativi e Bellman-Ford per i pesi negativi.

Dijkstra con ottimizzazione decrease-key

La versione di Dijkstra presentata nei manuali usa una coda con priorità dotata di decrease-key: quando la distanza di un nodo migliora, se ne aggiorna direttamente la priorità. Ciò richiede un heap di Fibonacci per ottenere O(E + V log V), ma è difficile da implementare. L'approccio della lazy deletion usato nei colloqui inserisce invece una nuova voce e ignora le estrazioni obsolete: è più semplice e comporta solo un sovraccarico di un fattore costante. In Python, la lazy deletion con heapq è l'implementazione standard nei colloqui tecnici.

Verifica rapida

Verifichi la propria comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep presentati in questa lezione.

Riepilogo della lezione

In questa lezione ha imparato che: Dijkstra usa un min-heap per elaborare i nodi secondo l'ordine della migliore distanza corrente, richiede O((V+E) log V) di tempo e non funziona con gli archi dal peso negativo e le voci obsolete dell'heap vengono gestite controllando un insieme visited al momento dell'estrazione. Nel prossimo argomento tratteremo Bellman-Ford, che gestisce i pesi negativi tramite n-1 passaggi di rilassamento.

Domande Frequenti

La lezione «Algoritmo di Dijkstra con coda di priorità» è gratuita?

Sì — il testo completo di «Algoritmo di Dijkstra con coda di priorità» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DSA Interview Prep, passa a CoddyKit PRO. Il corso DSA Interview Prep include 4 lezioni in totale.

Cosa imparerò in «Algoritmo di Dijkstra con coda di priorità»?

Implementi Dijkstra usando heapq, segua i passaggi di rilassamento su un grafo pesato e risolva il problema cheapest-flights-within-k-stops. Eserciti DSA Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare DSA Interview Prep?

Non è richiesta alcuna esperienza precedente. DSA Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.

Quanto tempo richiede la lezione «Algoritmo di Dijkstra con coda di priorità»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione DSA Interview Prep?

Sì. Ogni lezione DSA Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Algoritmo di Dijkstra con coda di priorità
  2. Bellman-Ford e cicli negativi
  3. Floyd-Warshall: cammini minimi tra tutte le coppie
  4. Network Delay Time e ricostruzione del percorso
← Torna a DSA Interview Prep