Problema del commesso viaggiatore: Python, C++ Algoritmo
โก Riepilogo intelligente
Il problema del commesso viaggiatore รจ un classico problema di ottimizzazione NP-difficile che richiede di trovare il percorso piรน breve che visiti ogni cittร esattamente una volta e ritorni al punto di partenza, utilizzando i dati di distanza forniti tramite un grafo.

Cosโรจ il problema del commesso viaggiatore (TSP)?
Il problema del commesso viaggiatore (TSP) รจ un classico problema di ottimizzazione combinatoria nell'informatica teorica. Dato un grafo di cittร , il TSP richiede di trovare il percorso piรน breve che visiti ogni nodo esattamente una volta e ritorni alla cittร di partenza.
Il problema viene descritto fornendo un elenco di cittร e le distanze tra ciascuna coppia di cittร .
Obiettivo: Parti dalla cittร di origine, visita ogni altra cittร esattamente una volta e torna alla cittร di partenza. L'obiettivo รจ trovare il percorso di andata e ritorno piรน breve possibile.
Esempio di TSP
Si consideri il grafico sottostante, dove 1, 2, 3 e 4 rappresentano le cittร e il peso di ciascun arco rappresenta la distanza tra tali cittร .
L'obiettivo รจ trovare il tour piรน breve possibile che parta dalla cittร di origine, visiti ogni altra cittร esattamente una volta e ritorni alla cittร di origine.
Per il grafico sopra, il percorso ottimale รจ 1-2-4-3-1. Il costo del tour piรน breve รจ 10 + 25 + 30 + 15 = 80.
Soluzioni diverse al problema del commesso viaggiatore
Il problema del commesso viaggiatore รจ classificato come NP-difficile perchรฉ nessun algoritmo noto in tempo polinomiale รจ in grado di risolverlo esattamente. La complessitร cresce esponenzialmente con il numero di cittร .
Esistono diversi modi per attaccare il TSP. Gli approcci piรน comuni sono:
Approccio basato sulla forza bruta: Il metodo ingenuo calcola ogni possibile percorso e li confronta. Il numero di percorsi in un grafo con n cittร รจ n!, il che rende il calcolo con la forza bruta estremamente oneroso per qualsiasi cosa che vada oltre una decina di cittร .
Metodo Branch and Bound: Il problema viene scomposto in sottoproblemi e le soluzioni di questi sottoproblemi si combinano per formare una soluzione ottimale. Un'efficace potatura scarta i percorsi parziali che non riescono a battere il miglior costo corrente.
Questo tutorial dimostra che approccio di programmazione dinamica, che รจ la versione con memorizzazione dei risultati dell'algoritmo branch and bound e corrisponde all'algoritmo Bellman-Held-Karp.
Programmazione dinamica: Si tratta di un metodo esatto che cerca la soluzione ottimale riutilizzando la sovrapposizioneping risultati del sottoproblema. ร piรน lento rispetto a quello quasi ottimale metodi avidima restituisce sempre un percorso globalmente ottimale.
La complessitร computazionale di questo approccio รจ O(Nยฒ ร 2^N), argomento che approfondiremo piรน avanti nell'articolo.
Metodo del vicino piรน prossimo: Un approccio euristico greedy che sceglie sempre la cittร non ancora visitata piรน vicina. ร molto piรน economico della programmazione dinamica, ma non garantisce un percorso ottimale, quindi viene utilizzato per soluzioni quasi ottimali quando la velocitร รจ piรน importante del minimo esatto.
Algoritmo per il problema del commesso viaggiatore
Utilizziamo l'approccio della programmazione dinamica per risolvere il problema del commesso viaggiatore (TSP). Prima di iniziare l'algoritmo, chiariamo alcuni termini:
- Un grafico
G = (V, E)รจ un insieme di vertici e spigoli. Vรจ l'insieme dei vertici.Eรจ l'insieme degli spigoli.- I vertici sono collegati tramite bordi.
Dist(i, j)indica la distanza non negativa tra i vertici i e j.
Supponiamo che S sia un sottoinsieme di cittร tratte da {1, 2, 3, โฆ, n} dove i e j sono due cittร in quel sottoinsieme. Allora cost(i, S, j) รจ la lunghezza del percorso piรน breve che parte da i, visita ogni cittร di S esattamente una volta e termina in j.
Per esempio, cost(1, {2, 3, 4}, 1) indica il percorso piรน breve dove:
- La cittร di partenza รจ 1
- Le cittร 2, 3 e 4 vengono visitate una sola volta
- Il punto finale รจ 1
La ricorrenza della programmazione dinamica รจ:
- Impostato
cost(i, {}, i) = 0, il che significa che partiamo e finiamo in i con costo zero. - Quando
|S| > 1, definirecost(i, S, 1) = โperi โ 1, perchรฉ il costo effettivo del tour non รจ ancora noto. - Partendo dalla cittร 1, scegli la cittร successiva in modo che
cost(i, S, j) = min [ cost(i, S โ {i}, j) + dist(i, j) ]peri โ Sandi โ j.
Per il grafico sopra riportato, la matrice di adiacenza รจ la seguente:
| dist(i, j) | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 1 | 0 | 10 | 15 | 20 |
| 2 | 10 | 0 | 35 | 25 |
| 3 | 15 | 35 | 0 | 30 |
| 4 | 20 | 25 | 30 | 0 |
Ecco come procede l'algoritmo:
Passo 1) Il viaggio inizia nella cittร 1, visita ogni altra cittร una volta e ritorna alla cittร 1.
Passo 2) S รจ un sottoinsieme di cittร . Per ogni |S| > 1, inizializza cost(i, S, 1) = โ. Qui cost(i, S, j) indica un tour che inizia da i, visita le cittร in S una volta e raggiunge j. Partiamo dall'infinito perchรฉ la distanza รจ sconosciuta in questo punto. Quindi i valori sono:
cost(2, {3, 4}, 1) = โ significa che partiamo dalla cittร 2, attraversiamo le cittร 3 e 4 e raggiungiamo la 1, con un costo sconosciuto. Allo stesso modo:
cost(3, {2, 4}, 1) = โ
cost(4, {2, 3}, 1) = โ
Passo 3) Per ogni sottoinsieme di S, calcola:
cost(i, S, j) = min [ cost(i, S โ {i}, j) + dist(i, j) ]Durante la serata, j โ S and i โ j.
Questo รจ il tour di costo minimo che inizia da i, visita il sottoinsieme di cittร una volta e ritorna a j. Poichรฉ il tour inizia dalla cittร 1, il costo ottimale รจ cost(1, {other cities}, 1).
Analisi della ricorrenza passo dopo passo
Ora S = {1, 2, 3, 4}. Ci sono quattro elementi, quindi il numero di sottoinsiemi รจ 2^4 = 16Questi sottoinsiemi sono:
1) |S| = 0: {ฮฆ}
2) |S| = 1: {{1}, {2}, {3}, {4}}
3) |S| = 2: {{1, 2}, {1, 3}, {1, 4}, {2, 3}, {2, 4}, {3, 4}}
4) |S| = 3: {{1, 2, 3}, {1, 2, 4}, {2, 3, 4}, {1, 3, 4}}
5) |S| = 4: {{1, 2, 3, 4}}
Poichรฉ il tour inizia dalla cittร 1, possiamo scartare ogni sottoinsieme che contiene la cittร 1 durante il calcolo dei costi intermedi.
Il calcolo dell'algoritmo si svolge come segue:
1) |S| = ฮฆ:
- costo(2, ฮฆ, 1) = dist(2, 1) = 10
- costo(3, ฮฆ, 1) = dist(3, 1) = 15
- costo(4, ฮฆ, 1) = dist(4, 1) = 20
2) |S| = 1:
- costo(2, {3}, 1) = dist(2, 3) + costo(3, ฮฆ, 1) = 35 + 15 = 50
- costo(2, {4}, 1) = dist(2, 4) + costo(4, ฮฆ, 1) = 25 + 20 = 45
- costo(3, {2}, 1) = dist(3, 2) + costo(2, ฮฆ, 1) = 35 + 10 = 45
- costo(3, {4}, 1) = dist(3, 4) + costo(4, ฮฆ, 1) = 30 + 20 = 50
- costo(4, {2}, 1) = dist(4, 2) + costo(2, ฮฆ, 1) = 25 + 10 = 35
- costo(4, {3}, 1) = dist(4, 3) + costo(3, ฮฆ, 1) = 30 + 15 = 45
3) |S| = 2:
- costo(2, {3, 4}, 1) = min [ dist(2, 3) + costo(3, {4}, 1) = 35 + 50 = 85, dist(2, 4) + costo(4, {3}, 1) = 25 + 45 = 70 ] = 70
- costo(3, {2, 4}, 1) = min [ dist(3, 2) + costo(2, {4}, 1) = 35 + 45 = 80, dist(3, 4) + costo(4, {2}, 1) = 30 + 35 = 65 ] = 65
- costo(4, {2, 3}, 1) = min [ dist(4, 2) + costo(2, {3}, 1) = 25 + 50 = 75, dist(4, 3) + costo(3, {2}, 1) = 30 + 45 = 75 ] = 75
4) |S| = 3:
- costo(1, {2, 3, 4}, 1) = min [ dist(1, 2) + costo(2, {3, 4}, 1) = 10 + 70 = 80, dist(1, 3) + costo(3, {2, 4}, 1) = 15 + 65 = 80, dist(1, 4) + costo(4, {2, 3}, 1) = 20 + 75 = 95 ] = 80
Quindi la soluzione ottimale รจ 1-2-4-3-1.
Pseudo-codice
Algorithm: Traveling-Salesman-Problem
Cost (1, {}, 1) = 0
for s = 2 to n do
for all subsets S belongs to {1, 2, 3, ..., n} of size s
Cost (s, S, 1) = Infinity
for all i in S and i != 1
Cost (i, S, j) = min {Cost (i, S - {i}, j) + dist(i, j) for j in S and i != j}
Return min(i) Cost (i, {1, 2, 3, ..., n}, j) + d(j, i)
Implementazione in C/C++
Ecco l'implementazione in C++. La versione seguente corregge l'inizio della sorgente return bug, che ritornava dopo la primissima permutazione invece di enumerare tutti i tour.
#include <bits/stdc++.h> using namespace std; #define V 4 #define MAX 1000000 int tsp(int graph[][V], int s) { vector<int> vertex; for (int i = 0; i < V; i++) if (i != s) vertex.push_back(i); int min_cost = MAX; do { int current_cost = 0; int j = s; for (int i = 0; i < vertex.size(); i++) { current_cost += graph[j][vertex[i]]; j = vertex[i]; } current_cost += graph[j][s]; min_cost = min(min_cost, current_cost); } while (next_permutation(vertex.begin(), vertex.end())); return min_cost; } int main() { int graph[][V] = { { 0, 10, 15, 20 }, { 10, 0, 35, 25 }, { 15, 35, 0, 30 }, { 20, 25, 30, 0 } }; int s = 0; cout << tsp(graph, s) << endl; return 0; }
Produzione:
80
implementazione in Python
Migliori Python l'implementazione rispecchia l' C++ versione. Corregge la sorgente from itertools, import errore di battitura, la virgola fuori posto return all'interno del circuito interno e l'incavo fuori posto su s = 0.
from sys import maxsize from itertools import permutations V = 4 def tsp(graph, s): vertex = [] for i in range(V): if i != s: vertex.append(i) min_cost = maxsize for perm in permutations(vertex): current_cost = 0 k = s for j in perm: current_cost += graph[k][j] k = j current_cost += graph[k][s] min_cost = min(min_cost, current_cost) return min_cost graph = [[0, 10, 15, 20], [10, 0, 35, 25], [15, 35, 0, 30], [20, 25, 30, 0]] s = 0 print(tsp(graph, s))
Produzione:
80
Soluzioni accademiche al TSP
Gli informatici hanno trascorso decenni alla ricerca di algoritmi a tempo polinomiale migliorati per il problema del commesso viaggiatore. Finora, il problema del commesso viaggiatore rimane NP-difficile.
Diverse tecniche pubblicate riducono la complessitร pratica per specifiche famiglie di istanze del problema del commesso viaggiatore (TSP):
- Il TSP simmetrico classico viene risolto dal Metodo del suffisso zero.
- Migliori Algoritmo di ottimizzazione basato sulla biogeografia Utilizza strategie di migrazione per risolvere problemi di ottimizzazione che corrispondono al problema del commesso viaggiatore (TSP).
- Migliori Algoritmo evolutivo multi-obiettivo ร progettato per il problema del commesso viaggiatore multi-obiettivo e si basa su NSGA-II.
- Migliori Sistema multi-agente Questo approccio risolve il problema del commesso viaggiatore (TSP) per N cittร con risorse computazionali fisse.
- Migliori Euristica di Lin-Kernighan e il suo successore LKH fornire tour con una precisione del 2-3% rispetto all'ottimale per casi con milioni di cittร .
- Concordia Utilizza piani di taglio e l'algoritmo branch-and-cut per calcolare gli ottimi esatti per istanze di benchmark con decine di migliaia di cittร .
Applicazione del problema del commesso viaggiatore
Il problema del commesso viaggiatore si presenta nel mondo reale sia in forma pura che modificata. Alcune delle principali applicazioni sono:
- Pianificazione, logistica e produzione di microchip: I problemi di inserimento dei chip nell'industria dei microchip vengono modellati come varianti del problema del commesso viaggiatore (TSP) per minimizzare il tempo di spostamento del braccio robotico.
- Sequenziamento del DNA: Un TSP modificato viene utilizzato nel sequenziamento del DNA, dove le cittร rappresentano i frammenti di DNA e le distanze rappresentano la similaritร tra i frammenti.
- Astronomia: Gli astronomi utilizzano il TSP per ridurre al minimo il tempo impiegato per spostare i telescopi tra gli obiettivi di osservazione.
- Controllo ottimale: Le formulazioni TSP modellano problemi di controllo ottimo in cui รจ necessario rispettare molteplici vincoli, minimizzando al contempo il costo di attraversamento.
- Consegna dell'ultimo miglio: AmazonUPS e le app di consegna di cibo risolvono le varianti dinamiche del TSP per sequenziare le fermate per gli autisti.
- Prelievo in magazzino: Robot e operatori umani seguono percorsi ottimizzati dal sistema TSP (Total Service Provider) che riducono i tempi di spostamento all'interno dei centri di distribuzione.
Analisi della complessitร del TSP
- Complessitร temporale: L'approccio di programmazione dinamica Held-Karp risolve 2N sottoinsiemi per ogni nodo iniziale, dando
N ร 2^Nsottoproblemi. Ogni sottoproblema richiede un tempo lineare per essere combinato. Se il nodo di origine non รจ specificato, รจ necessario un ciclo esterno su N nodi. La complessitร temporale totale รจO(Nยฒ ร 2^N). - Complessitร spaziale: La tabella DP memorizza
C(S, i)per ogni sottoinsieme S dell'insieme dei vertici. Ce ne sono 2N sottoinsiemi per nodo, quindi la complessitร dello spazio รจO(N ร 2^N), che spesso viene scritto comeO(2^N)quando N viene considerato fisso.
Successivamente, scopri il Algoritmo del crivello di Eratostene.




