L'algoritmo di Dijkstra in Python & C++ (Esempio)

⚡ Riepilogo intelligente

L'algoritmo di Dijkstra calcola il percorso più breve da un singolo vertice sorgente a ogni altro vertice in un grafo pesato con archi non negativi. Questo metodo greedy è alla base di Google Routing su mappe, routing IP OSPF e innumerevoli casi d'uso per il percorso più breve in rete.

  • 🎯 Idea centrale: L'algoritmo di Dijkstra espande in modo avido il vertice non visitato più vicino, aggiornando le distanze dei vicini finché ogni nodo raggiungibile non ha il suo vero costo più breve dalla sorgente.
  • 🔄 Vs BFS e DFS: BFS e DFS trovano un percorso qualsiasi senza considerare i pesi degli archi, mentre Dijkstra minimizza il costo totale sugli archi ponderati.
  • 🧭 Esempio passo passo: Un grafico ponderato a 7 vertici, opportunamente elaborato, mostra come le distanze si aggiornano iterativamente e come il percorso 1-2-6-7 risulti vincente con un costo di 7.
  • 💻 Copertura linguistica: Entrambi i progetti editoriali di C++ and Python Le implementazioni dimostrano la versione con matrice di adiacenza e funzione di selezione a distanza minima.
  • ⚠️ Limitazione: L'algoritmo di Dijkstra fallisce con pesi degli archi negativi perché un nodo finalizzato non viene mai riconsiderato; per i grafi con pesi degli archi negativi, utilizzare l'algoritmo di Bellman-Ford.
  • 📊 Complessità: La versione ingenua con array ha una complessità temporale e spaziale di O(V²); una coda di priorità riduce il tempo a O(E log V) per i grafi sparsi.

Algoritmo del percorso più breve di Dijkstra

Qual è il percorso più breve o la distanza più breve?

Il percorso dal vertice di partenza al vertice di destinazione che ha un costo minimo è detto percorso più breve o distanza più breve. Nella teoria dei grafi, è possibile che esistano più percorsi da una sorgente a una destinazione. Tra questi percorsi, se ne esiste uno che ha un costo minimo, lo definiamo percorso più breve.

In questo contesto, per "costo" si intende il numero di nodi nel percorso o la somma dei costi su ciascun arco. Un percorso può avere uno o più archi. Il collegamento tra due vertici è chiamato "arco". Esistono diversi tipi di algoritmi per la ricerca del percorso più breve, come l'algoritmo di Dijkstra e l'algoritmo di Bellman-Ford.

In questa sezione, analizzeremo l'algoritmo di Dijkstra. Consideriamo il seguente grafo pesato:

Grafo pesato non orientato

Un grafico ponderato non orientato

  • Il termine "ponderato" indica il costo per spostarsi da un nodo all'altro. Ad esempio, per spostarsi dal nodo 1 al nodo 2, il costo o peso è 1.
  • Il percorso tra il nodo 1 e il nodo 2 è chiamato arco.
  • "Non orientato" significa che è possibile spostarsi da un nodo all'altro e tornare al nodo precedente. Quindi, se proviamo a trovare tutti i percorsi dal nodo 1 al nodo 7, saranno:
Itinerario o percorsoCosto
1-2-6-7(1+3+3) = 7
1-2-3-7(1+9+1) = 11
1-3-7(7+1) = 8
1-4-5-7(6+2+5) = 13

Tra questi quattro percorsi, possiamo notare che il primo ha un costo di 7. Pertanto, è il percorso più breve in termini di costo.

Percorso più breve

Percorso più breve

Come funziona l'algoritmo di Dijkstra

L'algoritmo di Dijkstra è in grado di trovare la distanza più breve sia nei grafi pesati diretti che in quelli non diretti. Questo algoritmo è "greedy" perché sceglie sempre il nodo più vicino o più corto rispetto all'origine. Il termine "greedy" significa che, tra una serie di risultati possibili, l'algoritmo sceglierà il migliore.

Qui stiamo cercando di trovare i percorsi più brevi tra tutti gli altri percorsi. Quindi, l'algoritmo di Dijkstra trova tutti i percorsi più brevi da un singolo nodo sorgente. Di conseguenza, si comporta come un algoritmo avido.

Nella sezione "esempio" qui sotto, troverete la procedura passo passo. Funziona nel seguente modo:

Passo 1) Inizializza il nodo iniziale con costo 0 e tutti gli altri nodi con costo infinito.
Passo 2) Mantenere un array o una lista per conservare track dei nodi visitati.
Passo 3) Aggiorna il costo del nodo con il costo minimo. Puoi farlo confrontando il costo attuale con il costo del percorso (come mostrato nella sezione degli esempi).
Passo 4) Continuare con il passaggio 3 finché non sono stati visitati tutti i nodi.

Dopo aver completato tutti questi passaggi, troveremo il percorso che costa il minimo dalla fonte alla destinazione.

Differenza tra Dijkstra e BFS, DFS

La principale differenza tra Dijkstra e BFS-DFS è che Dijkstra è un algoritmo di ricerca del percorso più breve, mentre BFS e DFS sono algoritmi di ricerca del percorso più generici. In generale, BFS e DFS non considerano il costo degli archi durante la ricerca del percorso. Pertanto, questi algoritmi non possono garantire il percorso più breve.

Dimostrazione tramite griglia 2D del funzionamento dell'algoritmo BFS.

Dimostrazione della griglia 2D BFS

Algosketch, che mostra la dimostrazione BFS

Questa dimostrazione indica che BFS trova solo il percorso. Tuttavia, non si preoccupa del peso del percorso. BFS (Ricerca in base all'ampiezza) presuppone che viaggiare da un nodo a un altro nodo costerà solo 1.

Vediamo un esempio di grafico:

Esempio di grafico dimostrativo di una griglia 2D

Qui, BFS trova un percorso al livello 2. BFS attraversa il grafo in ordine di livello. Quindi, procede in questo modo:

Passo 1) Parti dal nodo “1” e visita tutti i nodi adiacenti 2, 3, 4.

Passo 2) Contrassegna i nodi 2, 3 e 4 come livello 1 e visita i nodi adiacenti. Continuerà ad esplorare tutti i nodi adiacenti fino a raggiungere il nodo di destinazione.

In termini di DFS, attraverserà il percorso da 1 a 7 come segue:

  • 1→2→3→7 (Costo originale 10, costo DFS 3)
  • 1→2→6→7 (Costo originale 7, costo DFS 3)
  • 1→3→7 (Costo originale 8, costo DFS 2)
  • 1→4→5→7 (Costo originale 13, costo DFS 3)

Come possiamo vedere, DFS calcola il costo del percorso in base al numero di archi. DFS esegue le seguenti operazioni:

  • DFS può trovare un percorso dall'origine (vertice iniziale) alla destinazione.
  • Non può garantire se il percorso scoperto dal nodo di origine alla destinazione sia il percorso più breve o meno.

Tuttavia, per quanto riguarda l'algoritmo di Dijkstra, esso seleziona gli archi in base al loro costo. Essendo un algoritmo greedy, sceglierà i percorsi con il costo minimo.

Esempio dell'algoritmo di Dijkstra

L'algoritmo di Dijkstra utilizza il costo o il peso per calcolare il costo totale del percorso.

Esempio dell'algoritmo di Dijkstra

L'obiettivo dell'algoritmo di Dijkstra è ridurre al minimo questo costo o peso totale. Nell'esempio mostrato sopra, troviamo i percorsi migliori dal nodo 1 al nodo 7, quindi calcoliamo tutti i costi.

Nell'algoritmo di Dijkstra, i percorsi più brevi vengono calcolati tramite pesi. Non vengono esplorati tutti i percorsi possibili. Illustriamo l'algoritmo di Dijkstra con un esempio. Supponiamo di dover trovare il percorso più breve dal nodo 1 al nodo 7.

Per questo processo, i passaggi sono indicati di seguito:

Passo 1) Inizializza il costo del nodo iniziale a 0. Assegna “Inf” al resto dei nodi. Significa che non esiste un percorso tra la sorgente e il nodo, oppure che il percorso non è ancora stato visitato.

Inizializzazione dell'algoritmo di Dijkstra

Passo 2) Quando selezioni il nodo 1, questo verrà contrassegnato come visitato. Quindi aggiorna tutti i nodi adiacenti al nodo 1. 2, 3, 4 sono i nodi adiacenti al nodo 1.

Durante l'aggiornamento di un costo, dobbiamo seguire la procedura seguente:

Procedura di aggiornamento dell'algoritmo di Dijkstra

Possiamo aggiornare il costo di ciascun nodo utilizzando la formula sopra riportata. Ad esempio, ci trovavamo al nodo 1 e dovevamo aggiornare il costo dei nodi adiacenti 2, 3 e 4. Dopo l'aggiornamento, i costi appariranno così:

Algoritmo di Dijkstra dopo il primo aggiornamento

Passo 3) Per il nodo "2", i vicini sono 6 e 3. Stiamo aggiornando il costo al nodo "6" confrontando infinito (valore corrente) con il costo del nodo 2 più il costo del percorso da 2 a 6. In parole semplici, il nodo "6" avrà un costo pari a 1+3, ovvero 4.

Aggiornamento dell'algoritmo di Dijkstra, nodo 6

Il nodo 3 è vicino al nodo 2. Tuttavia, abbiamo calcolato il suo costo nel passaggio precedente, che era 7. Ora, se il nostro percorso è 1-2-3, il nodo 3 avrà un costo pari a 10. Percorso 1-2- 3 costeranno 10, mentre da 1 a 3 costeranno 7.

Passo 4) Per il nodo 3, il nodo adiacente è 7. Quindi, confrontando il valore corrente del nodo 7 con il costo del percorso (7+1) ovvero 8, aggiorneremo il costo del nodo 7. Cioè 8. Quindi, troviamo un percorso dal nodo 1 al nodo 7, che è 1→3→7. Il costo è 8.

Passo 5) Per il nodo 4, aggiorneremo di conseguenza il costo del nodo adiacente. Quindi, il nodo "5" avrà un costo aggiornato pari a 8. Dopo i passaggi 4 e 5, il risultato sarà il seguente:

Algoritmo di Dijkstra dopo il passaggio 4 5

Ora, il percorso 1-3-7 ha un costo di 8 (in precedenza). Il nodo "7" non è stato contrassegnato come visitato perché possiamo raggiungere il nodo "7" dal nodo "6". Il percorso "1-2-6" aveva un costo di 4. Quindi il percorso 1-2-6-7 avrà un costo di 7.

Poiché 7 < 8, il percorso più breve dal vertice di partenza "1" al vertice di destinazione "7" sarà 1-2-6-7 e il costo è 7. In precedenza era 1-3-7 e il costo era 8. Quindi, il grafo finale sarà il seguente:

Grafico finale dell'algoritmo di Dijkstra

Il bordo contrassegnato da una linea nera è il nostro percorso più breve da 1 a 7 e ci costerà 7.

Soprannome Code Algoritmo di Dijkstra

Ecco lo pseudocodice dell'algoritmo di Dijkstra:

Dijkstra(G, S):
  for each vertex V in G
    distance[V] <- Infinity
    previous[V] <- NULL
    if V does not equal S, then,
      (priority queue) Q.push(V)
  distance[S] = 0
  While Q is not empty
    U <- Extract the MIN from Q
    For each unvisited adjacent V of U
      TotalDistance <- distance[U] + edge_cost(U, V)
      if TotalDistance is less than distance[V], then
        distance[V] <- TotalDistance
        previous[V] <- U
  return distance, previous

C++ Implementazione dell'algoritmo di Dijkstra

Per implementare l'algoritmo di Dijkstra utilizzando C++Ecco il codice:

#include <bits/stdc++.h>
using namespace std;
#define size 7
int minimumDistance(int distance[], bool visited[]) {
  int min = INT_MAX;
  int min_index = INT_MAX;
  for (int i = 0; i < size; i++) {
    if (!visited[i] && distance[i] <= min) {
      min = distance[i];
      min_index = i;
    }
  }
  return min_index;
}
void printParentPath(int parent[], int i) {
  if (parent[i] == -1) {
    return;
  }
  printParentPath(parent, parent[i]);
  cout << i + 1 << " ";
}
void dijkstra(int graph[size][size], int source) {
  int distance[size];
  bool visited[size];
  int parent[size];
  for (int i = 0; i < size; i++) {
    parent[0] = -1;
    distance[i] = INT_MAX;
    visited[i] = false;
  }
  distance[source] = 0;
  for (int i = 0; i < size - 1; i++) {
    int U = minimumDistance(distance, visited);
    visited[U] = true;
    for (int j = 0; j < size; j++) {
      int curr_distance = distance[U] + graph[U][j];
      if (!visited[j] && graph[U][j] &&
          curr_distance < distance[j]) {
        parent[j] = U;
        distance[j] = curr_distance;
      }
    }
  }
  cout << "Vertex\t\tDistance\tPath" << endl;
  for (int i = 1; i < size; i++) {
    cout << source + 1 << "->" << i + 1 << "\t\t" << distance[i] << "\t\t"
         << source + 1 << " ";
    printParentPath(parent, i);
    cout << endl;
  }
}
int main() {
  int graph[size][size] = {{0, 1, 7, 6, 0, 0, 0}, {1, 0, 9, 0, 0, 3, 0},
                           {7, 9, 0, 0, 0, 0, 1}, {6, 0, 0, 0, 2, 0, 0},
                           {0, 0, 0, 2, 0, 0, 0}, {0, 3, 0, 0, 0, 0, 3},
                           {0, 0, 0, 0, 5, 3, 0}};
  dijkstra(graph, 0);
}

Produzione:

Vertex     Distance        Path

1->2           1             1 2
1->3           7             1 3
1->4           6             1 4
1->5           8             1 4 5
1->6           4             1 2 6
1->7           7             1 2 6 7

Python Implementazione dell'algoritmo di Dijkstra

Per implementare l'algoritmo di Dijkstra utilizzando PythonEcco il codice:

num_of_vertex = 7
def minimumDistance(distance, visited):
    _min = 1e11
    min_index = 1e11
    for i in range(num_of_vertex):
        if not visited[i] and distance[i] <= _min:
            _min = distance[i]
            min_index = i
    return min_index

def printParentNode(parent, i):
    if parent[i] == -1:
        return
    printParentNode(parent, parent[i])
    print("{} ".format(i + 1), end="")

def dijkstra(graph, src):
    distance = list()
    visited = list()
    parent = list()
    for i in range(num_of_vertex):
        parent.append(-1)
        distance.append(1e11)
        visited.append(False)
    distance[src] = 0
    for i in range(num_of_vertex - 1):
        U = minimumDistance(distance, visited)
        visited[U] = True
        for j in range(num_of_vertex):
            curr_distance = distance[U] + graph[U][j]
            if not visited[j] and graph[U][j] and curr_distance < distance[j]:
                parent[j] = U
                distance[j] = curr_distance
    print("Vertex\t\tDistance\tPath")
    for i in range(num_of_vertex):
        print("{}->{}\t\t{}\t\t{} ".format(src + 1, i + 1, distance[i], src + 1), end="")
        printParentNode(parent, i)
        print("")

graph = [
    [0, 1, 7, 6, 0, 0, 0],
    [1, 0, 9, 0, 0, 3, 0],
    [7, 9, 0, 0, 0, 0, 1],
    [6, 0, 0, 0, 2, 0, 0],
    [0, 0, 0, 2, 0, 0, 0],
    [0, 3, 0, 0, 0, 0, 3],
    [0, 0, 0, 0, 5, 3, 0]
]
dijkstra(graph, 0)

Produzione:

Vertex     Distance        Path

1->1           0              1
1->2           1              1 2
1->3           7              1 3
1->4           6              1 4
1->5           8              1 4 5
1->6           4              1 2 6
1->7           7              1 2 6 7

Possiamo osservare che l'algoritmo calcola la distanza più breve dal nodo sorgente.

Applicazione dell'algoritmo di Dijkstra

L'algoritmo di Dijkstra ha una vasta gamma di applicazioni. Tra queste, è ampiamente utilizzato nel campo delle reti. Ecco alcuni esempi concreti di utilizzo dell'algoritmo di Dijkstra:

Dijkstra in Google Mappe: Questo algoritmo costituisce la base per la ricerca dei percorsi più brevi, come si può notare dall'output del frammento di codice riportato sopra.

Applicazione dell'algoritmo di Dijkstra Google Maps

Google non utilizza il semplice algoritmo di Dijkstra. Invece, utilizza una versione modificata. Quando selezioni una destinazione, ti mostra più percorsi in Google Mappe. Tra questi percorsi, alcuni vengono ordinati per l'utente. Questi percorsi vengono selezionati in base al "tempo". Quindi, il "tempo" rappresenta un costo di collegamento per il percorso più breve.

Dijkstra nel routing IP: Instradamento IP Il routing IP è un termine tecnico di rete. Descrive come un pacchetto di dati viene inviato al destinatario attraverso diversi percorsi. Questi percorsi sono costituiti da router, server e altre apparecchiature. Nel routing IP esistono diversi tipi di protocolli.

Questi protocolli aiutano il router a trovare i percorsi più brevi per inviare i dati. Uno di questi protocolli è "OSPF (Open Shortest Path First)". OSPF utilizza l'algoritmo di Dijkstra. Il router mantiene una tabella di percorsi. Ogni router condivide la propria tabella con i router vicini. Dopo aver ricevuto la tabella aggiornata, questi devono ricalcolare tutti i percorsi. In questa fase, il router utilizza l'algoritmo di Dijkstra.

Limitazione dell'algoritmo di Dijkstra

L'algoritmo di Dijkstra non può garantire il percorso più breve in un grafo con archi negativi. L'algoritmo di Dijkstra si basa sui seguenti principi:

  • Da un nodo all'altro verrà preso il percorso più breve.
  • Una volta selezionato il percorso più breve tra due nodi, questo non verrà ricalcolato.

Qui, notate due esempi con bordi negativi.

Limitazione dei bordi negativi dell'algoritmo di Dijkstra

Nel grafico a sinistra, Ci sono tre vertici. L'algoritmo di Dijkstra verrà eseguito sul grafo nel modo seguente:

Passo 1) Il vertice iniziale “1” verrà inizializzato a zero. Gli altri nodi avranno infinito.

Limitazione dell'algoritmo di Dijkstra, passo 1

Passo 2) Contrassegna il nodo "1" come visitato e includilo nel percorso più breve.

Passo 3) La distanza del nodo sorgente 1 dai nodi "2" e "3" è impostata a infinito, poiché il percorso più breve deve ancora essere calcolato. Pertanto, qualsiasi percorso con un costo inferiore a infinito verrà aggiunto al percorso più breve (approccio greedy).

Passo 4) Aggiornamento della distanza dal vertice sorgente "1" al "2". Il peso corrente sarà 5 (5 < infinito). Analogamente, aggiornamento della distanza dal nodo "1" al "3" con peso 3.

Limitazione dell'algoritmo di Dijkstra, passo 4

Passo 5) Ora, se controlliamo le distanze più brevi dal nodo "1", scopriamo che 5 è la distanza più breve per l'arco 1→2. Quindi, il nodo "2" verrà contrassegnato come visitato. Allo stesso modo, anche il nodo "3" verrà contrassegnato come visitato poiché la distanza più breve è 3.

Tuttavia, se osserviamo, esiste un percorso 1-3-2 che costa solo 2. Ma Dijkstra mostra che dal nodo "1" al nodo "2" la distanza più breve è 5. Quindi, Dijkstra non è riuscito a calcolare correttamente la distanza più breve. Il motivo è che Dijkstra è un algoritmo greedy. Pertanto, una volta che un nodo viene contrassegnato come visitato, non verrà riconsiderato, anche se potrebbe essere disponibile un percorso più breve. Questo problema si verifica solo quando gli archi hanno costi negativi o pesi negativi.

In questo scenario, l'algoritmo di Dijkstra non riesce a calcolare il percorso più breve tra due nodi. Di conseguenza, presenta alcuni svantaggi. Per risolvere il problema degli archi negativi, si utilizza un altro algoritmo chiamato "algoritmo di Bellman-Ford", che è in grado di gestire anche gli archi negativi.

Complessità dell'algoritmo di Dijkstra

L'implementazione sopra ha utilizzato due cicli "for". Questi cicli vengono eseguiti per il numero di vertici. Quindi, la complessità temporale è O(V²)In questo contesto, il termine “O” è una notazione che fornisce un'ipotesi per l'algoritmo di Dijkstra.

Possiamo memorizzare il grafo utilizzando una "coda di priorità". Una coda di priorità è una struttura dati heap binaria. Sarà più efficiente di una matrice 2D. Un arco con un costo minimo avrà un'alta priorità. Quindi la complessità temporale sarà O(E log V). Dove E è il numero di spigoli e V è il numero di vertici.

La complessità dello spazio è O(V²), poiché stiamo utilizzando una matrice di adiacenza (matrice 2D). La complessità dello spazio può essere ottimizzata utilizzando una lista di adiacenza o una struttura dati di coda.

DOMANDE FREQUENTI

Gli agenti di pianificazione del percorso basati sull'IA nella robotica, nei veicoli autonomi e nei personaggi non giocanti (NPC) dei videogiochi utilizzano l'algoritmo di Dijkstra per trovare i percorsi a costo minimo su grafi pesati. Anche gli ambienti di apprendimento per rinforzo si basano su di esso per calcolare i percorsi di riferimento ottimali per la condivisione delle ricompense.ping e valutazione.

Sì. Gli assistenti di programmazione AI come GitHub Copilot e GPT possono generare l'algoritmo di Dijkstra in Python, C++, o Java, comprese le varianti con coda di priorità che utilizzano heap. Possono anche stampare il percorso più breve effettivo o adattare il codice a grafi memorizzati come liste di adiacenza.

Utilizzando un semplice array per trovare il nodo minimo, l'algoritmo di Dijkstra ha una complessità temporale di O(V²). Con una coda di priorità heap binaria, la complessità scende a O((V + E) log V), mentre con un heap di Fibonacci raggiunge O(E + V log V), risultando ottimale per i grafi sparsi.

L'algoritmo di Dijkstra finalizza un vertice non appena seleziona la distanza minima corrente. Un arco negativo successivo potrebbe rendere più economico un percorso più lungo, ma il vertice finalizzato non viene mai più visitato, quindi l'algoritmo riporta una distanza minima errata.

Scegliete Dijkstra quando ogni peso dell'arco è non negativo perché è più veloce con una complessità O((V+E) log V). Scegliete Bellman-Ford quando gli archi possono essere negativi o quando è necessario rilevare cicli con peso negativo; il suo tempo di esecuzione O(V·E) rappresenta il compromesso.

Google Le mappe utilizzano varianti e successori di Dijkstra, tra cui A* e ContracGerarchie di zione, ottimizzate per le reti stradali e il traffico reale. L'idea di base dell'espansione greedy tramite il minimo costo cumulativo rimane il contributo principale di Dijkstra.

L'algoritmo A* estende l'algoritmo di Dijkstra aggiungendo una stima euristica della distanza dall'obiettivo, espandendo un numero inferiore di nodi quando è disponibile una buona euristica. Dijkstra esplora in tutte le direzioni, mentre A* orienta la ricerca verso l'obiettivo, rendendola di fatto più veloce.

Oltre alle mappe, Dijkstra è alla base dei protocolli di routing OSPF e IS-IS su Internet, dell'ottimizzazione della topologia di rete, dell'instradamento delle chiamate telefoniche, della pianificazione del movimento dei robot, delle query di connessione più breve sui social network e della minimizzazione dei costi dei voli aerei.

Riassumi questo post con: