Vous êtes sur la page 1sur 22

See discussions, stats, and author profiles for this publication at: https://www.researchgate.

net/publication/358600281

Problème du Voyageur de Commerce (PVC): Etude théorique du problème &


Etude Expérimentale.

Article · February 2022

CITATIONS READS

0 595

1 author:

Iyad Chehili
University of Science and Technology Houari Boumediene
3 PUBLICATIONS   0 CITATIONS   

SEE PROFILE

All content following this page was uploaded by Iyad Chehili on 15 February 2022.

The user has requested enhancement of the downloaded file.


Problème du voyageur de commerce:
Etude théorique du problème :
Historique et présentation du problème :
Le problème du voyageur de commerce est un problème d’optimisation de la famille des NP-
complets, reconnu comme un grand classique de la recherche scientifique.
Si des algorithmes simples de résolution existent, ils n’en demeurent pas moins
problématiques dès que la dimension du problème croît, la complexité de ces algorithmes
étant telle que l’on obtient très rapidement des temps de calcul infinitésimaux. De nombreux
travaux ont donc été réalisés afin d’élaborer des algorithmes d’approximation permettant de
retourner une solution proche de l’optimum dans des temps de calcul beaucoup plus
raisonnables.
Un voyageur de commerce doit visiter n villes données en passant par chaque ville
exactement une fois. Il commence par une ville quelconque et termine en retournant à la ville
de départ. Les distances entre les villes étant connues, la question que l’on se pose est de
savoir quel chemin il faut choisir pour minimiser la distance totale parcourue.

Figure 1 exemple de 10 villes

De très nombreuses applications logistiques concrètes dérivent plus ou moins directement de


ce problème. L’établissement de lignes de bus et métro d’une ville, le partage du champ
d’action d’une société de livraison en plusieurs secteurs, l’établissement de missions pour les
chariots cavaliers dans le cadre du rangement d’un terminal à conteneurs, etc. Si ce problème
paraît simple en apparence, la réalité mathématique est tout autre. En effet, le problème du
voyageur de commerce est un problème dit NP-complet. Cela signifie qu’il est possible de
vérifier une solution en un temps polynomial mais que le temps nécessaire pour trouver la
solution est exponentiel.
Dans notre cas, pour un nombre n de villes, il existe n-1! parcours existants. Par exemple,
pour un problème à 69 villes, le nombre de chemins possibles est un nombre à 100 chiffres.
Pour comparaison, un nombre d’une longueur de 80 chiffres permettrait déjà de représenter le
nombre d’atomes dans tout l’univers connu.
Historique :
 𝟏𝟗 𝑖é𝑚é Siècle : Les premières approches mathématiques exposées pour le PVC ont été
traitées au 19 𝑖é𝑚é siècle par les mathématiciens Sir William Rowan Hamilton et Thomas
Penyngton Kirkman. Hamilton en a fait un jeu : Hamilton Icosian Game. Les joueurs devaient
réaliser une tournée passant par 20 points en utilisant uniquement les connections Préséniles.
 Années 1930 : Le PVC est trait´e plus en profondeur par Karl Menger ` Harvard. Il est
ensuite d´enveloppe à Princeton par les mathématiciens Hassler Whitney et Merrill
Flood.
 1954 : Solution du PVC pour 49 villes par Dantzig, Fulkerson et Johnson par la méthode
du cutting-plane.
 1975 : Solution pour 100 villes par Camerini, Fratta et Maffioli.
 1987 : Solution pour 532, puis 2392 villes par Padberg et Rinaldi.
 1998 : Solution pour les 13 509 villes des Etats-Unis.
 2001 : Solution pour les 15 112 villes d’Allemagne par Applegate, Bixby Chvtal et
Cook des universités de Rice et Princeton.

Définition Formelle du TSP :


Le problème du voyageur de commerce TSP peut être défini formellement comme suit :
Instance : Un graphe de villes non orienté G= (V, D) où V= {v1, v2, …, vn} est l'ensemble de
villes, D= {d (vi, vj) / d (vi, vj) est la distance entre les villes vi et vj} l'ensemble des distances
entre les villes et un nombre entier k.
Question : le voyageur de commerce doit visiter toutes les villes une et une seule fois et
revenir à la ville de départ v1. Existe-il un circuit que le voyageur peut traverser tel que la
distance totale parcourue est inférieure ou égale à k ?

La modélisation de la solution (Structure de données de la solution) :


En utilisant l’approche par la force brute :

On doit établir une démarche générale permettant de résoudre le problème pour n'importe
quel ensemble de villes : cette démarche s'appelle un algorithme.
Les algorithmes pour résoudre le problème du voyageur de commerce peuvent être répartis en
deux classes :
- les algorithmes déterministes qui trouvent la solution optimale.
- les algorithmes d'approximation qui fournissent une solution presque optimale. Elles
permettent d'obtenir en un temps rapide de bonnes solutions, pas nécessairement optimales
mais de qualité suffisante.
Pour cette partie du projet, il est demandé de faire l’étude d’une méthode de résolution exacte
d’un problème NP-complet.

Nous passerons par l'approche naïve de la force brute pour résoudre le problème en utilisant
un concept mathématique connu sous le nom de "permutation".
Nous pouvons visualiser le problème en créant une structure de données de graphe ayant des
nœuds et des arêtes pondérées comme longueurs de chemin

Figure 2graphe ayant des nœuds et des arêtes


pondérées comme longueurs de chemin

Par exemple, les nœuds 2 à 3 prennent une arête pondérée de 17.


Nous devons trouver le chemin le plus court couvrant tous les nœuds exactement une fois, ce
qui est mis en évidence dans la figure ci-dessous

Figure 3 le chemin le plus court couvrant tous les nœuds exactement une fois

Il y a quelques étapes classiques et faciles que nous devons suivre pour résoudre le problème
TSP.
Présentation de l’algorithme de résolution :
Les étapes d’exécution :
Vous aurez besoin d'un tableau à deux dimensions pour obtenir la matrice d’adjacence du
graphe donné.
 Obtenez le nombre total de nœuds et le nombre total d'arêtes dans deux variables, à savoir
num_nodes et num_edges.
 Créez un tableau multidimensionnel edge_list ayant la dimension égale à num_nodes *
num_nodes
 Exécutez une boucle num_nodes time et prenez deux entrées, à savoir first_node et
second_node * à chaque fois comme deux nœuds ayant un bord entre eux et placez la
position edge_list[first_node] [second_node] égale à 1.
 Après l'exécution de la boucle, nous avons une matrice d’adjacence disponible, c'est-à-
dire edge_list.

debut
// Obtenir le nombre de nœuds et le nombre d'arêtes en entrée
num_nodes, num_edges,i,j: entier ;
lire(num_nodes, num_edges) ;
//créer une matrice
edges_list[i][j] matrice d’entier;
pour i de 0 a num_nodes faire
debut
pour j de 0 a num_nodes faire
debut
edges_list[i][j]  0;
fin
fin
// mécanisme de remplissage de la matrice adjacente
pour i de 0 a num_edges faire
debut
first_node, second_node, weight : entier;
lire(first_node, second_node, weight) ;
edges_list[first_node] [second_node]  weight;
edges_list[second_node] [first_node]  weight;
fin
fin
Figure 4 pseudo code pour trouver la matrice adjacente du graphe

L'étape la plus importante dans la conception de l'algorithme de base est celle-ci.


 Considérant une ville source de départ, à partir de laquelle le vendeur partira. Nous
pouvons considérer n'importe quelle ville comme point de départ et par défaut nous avons
considéré 0 comme point de départ ici.
 Générer la permutation des villes de repos. Supposons que nous ayons un total de N
nœuds et que nous ayons considéré un nœud comme source, alors nous devons générer le
reste (N-1) ! (Factorielle de N-1) permutations.
 Nous devons calculer la somme des contours (somme du chemin) pour chaque
permutation et suivre la somme minimale du chemin avec chaque permutation.
 Renvoie le coût marginal minimum.
Debut
Public class brute_force
Debut
Fonction shortest_path_sum
Entree : edges_list : matrice [1..n][1..n] d’entier ;
num_nodes :entier ;
sortie : shortest_path : entier
debut
//Choisir une ville source
source : entier ;
nodes vecteur [1..n] d’entier;
// pousser le reste des villes num_nodes-1 dans un paquet
Pour i de 0 a num_nodes faire
debut
si (i <> source) alors
debut
nodes.push_back(i);
fin_si
fin
n, shortest_path: entier;
n  nodes.size();
shortest_path INT_MAX;
//générer des permutations et suivre le coût minimum
Tantque (next_permutation(nodes.begin(), nodes.end())) faire
debut
i j,path_weight : entier;
path_weight0;
j source;
pour i de 0 a n faire
debut
path_weight  path_weight + edges_list[j][nodes[i]];
j  nodes[i];
finpour
path_weight  path_weight +edges_list[j][source];
shortest_path  min (shortest_path, path_weight);
fintantque
retourner(shortest_path) ;
fin_fonction
fin

Figure 5 pseudo code pour le chemin le plus court

next_permutation(), qui prend deux itérateurs bidirectionnels à savoir (ici vector::iterator)


nodes.begin() et nodes.end().
Cette fonction renvoie un type booléen (c'est-à-dire vrai ou faux).
Mécanisme de travail :
Cette fonction réorganise les objets dans [nodes.begin(), nodes.end()], où le [] représente les
deux itérateurs inclus, dans un ordre lexicographique. S'il existe un arrangement
Lexicographique plus grand que l'arrangement actuel alors la fonction renvoie vrai sinon elle
renvoie faux.
L'ordre lexicographique est également connu sous le nom d'ordre du dictionnaire en
mathématiques.

Code principal :
Public class brute_force
Debut
Fonction shortest_path_sum
Entree : edges_list : matrice [1..n][1..n] d’entier ;
num_nodes :entier ;
sortie : shortest_path : entier
debut
//Choisir une ville source
source : entier ;
nodes vecteur [1..n] d’entier;
// pousser le reste des villes num_nodes-1 dans un paquet
Pour i de 0 a num_nodes faire
debut
si (i <> source) alors
debut
nodes.push_back(i);
fin_si
fin
n, shortest_path: entier;
n  nodes.size();
shortest_path INT_MAX;
//générer des permutations et suivre le coût minimum
Tantque (next_permutation(nodes.begin(), nodes.end())) faire
debut
i j,path_weight : entier;
path_weight0;
j source;
pour i de 0 a n faire
debut
path_weight  path_weight + edges_list[j][nodes[i]];
j  nodes[i];
finpour
path_weight  path_weight +edges_list[j][source];
shortest_path  min (shortest_path, path_weight);
fintantque
retourner(shortest_path) ;
fin_fonction
fin

var

debut
// Obtenir le nombre de nœuds et le nombre d'arêtes en entrée
num_nodes, num_edges,i,j: entier ;
lire(num_nodes, num_edges) ;
//créer une matrice
edges_list[i][j] matrice d’entier;
pour i de 0 a num_nodes faire
debut
pour j de 0 a num_nodes faire
debut
edges_list[i][j]  0;
fin
fin
// mécanisme de remplissage de la matrice adjacente
pour i de 0 a num_edges faire
debut
first_node, second_node, weight : entier;
lire(first_node, second_node, weight) ;
edges_list[first_node] [second_node]  weight;
edges_list[second_node] [first_node]  weight;
fin

pour i de 0 a num_nodes faire


debut
pour j de 0 a num_nodes faire
debut
ecrire (edges_list[i][j]);
finpour
finpour
approach1: de type brute_force;
ecrire(approach1.shortest_path_sum(edges_list,num_nodes));
fin
Figure 6 Main Code

Complexité théorique :
 Temporelle :
Si l’ordinateur peut calculer un million de circuits Hamiltonien par seconde.
N=6 ;7 ;8 ;9 : instantanée
N=10 : environ 1/3 seconde
N=11 : environ 4 secondes
N=12 : environ 40 seconds
N=13 : environ 8 minutes
N=14 : près de 2 heures
N=15 : un peu plus d'une journée
N=20 : plus d'un million d'années
La complexité temporelle de l'algorithme dépend du nombre de nœuds. Si le nombre de
nœuds est n alors la complexité temporelle sera proportionnelle à N-1 ! (Factorielle de N-1)
soit O (N!).

 Spatiale :
La plus grande quantité d'espace dans cet algorithme de graphe est prise par la matrice
adjacente qui est une matrice à deux dimensions N*N, où n est le nombre de nœuds. La
complexité spatiale est donc O(N2).

Présentation de l’algorithme de vérification :


- Entrer le nombre des nœuds puis arêtes.
- Entrer les poids des arêtes d’un paire de sommet
- Entrer la distance minimale.
- Entrer le chemin. (L’algorithme arête une fois le sommet de départ répété)
- L’algorithme doit faire un test sur le nombre des sommets parcourus et s’il y a des
sommets qui répété plus d’une fois et que puis calculer la distance parcourue et
comparer avec la distance minimale donner au début.

Pseudo code :
int main()
{
/// Getting the number of nodes and number of edges as input
int num_nodes,num_edges;
cin >> num_nodes >> num_edges; ///O(1)

/// creating a multi-dimensional array


int** edges_list = new int*[num_nodes];

for(int i=0;i<num_nodes;i++)
{
edges_list[i] = new int[num_nodes];
for(int j=0;j<num_nodes;j++)
{
edges_list[i][j] = 0; ///O(N²)
}
}

/// adjacent matrix filling mechanism


for(int i=0;i<num_edges;i++)
{
int first_node,second_node,weight;
cin >> first_node >> second_node >> weight; ///O((N²-N)/2)
edges_list[first_node][second_node] = weight;
edges_list[second_node][first_node] = weight;

}
int a[100],distance=0;
for(int i=0;i<num_nodes;i++)
{
for(int j=0;j<num_nodes;j++)
{
cout << edges_list[i][j] << " "; ///O(N²)
}
cout << endl;
}
cout << endl << endl;/*
brute_force approach1;
cout << approach1.shortest_path_sum(edges_list,num_nodes) << endl;*/
///algorithme de verification

bool doubl=true;
int k,i=1;
//entrer la distance minimale
cout<<"entrer la distance minimale:"; ///O(1)
cin>>k; ///O(1)
//entrer le chemin
cout<<"donner le chemin:";
cin>>a[0];
do{
cin>>a[i]; ///O(n)
i++;
}while(a[i-1]!=a[0]);
//calcul de la distance
for(int j=0;j<i;j++){
distance+=edges_list[a[j]][a[(j+1)%i]]; ///O(n)
}
//tester si la distance est minimale ou non
if(distance<=k){ ///O(1)
cout<<"\nle chemin est "<<distance<<" il est optimale";}
else{cout<<"\nle chemin est "<<distance<<" il n'est pas optimale";}
//teste si un sommet est parcouru plus d'une fois
for(int l=1;l<i;l++){
for(int q=1;q<i;q++){
if(l!=q&&a[l]==a[q])doubl=false; ///O(n²)
}
}
//affichage des testes
if(i==num_nodes||!doubl) { ///O(n)
cout<<"\n mais tu ne pas parcouru toutes les sommets";}
else{
cout<<"\n tous les sommets sont parcourus";}
///end algorithm
return 0;
}
Figure 7 pseudo code pour l’algorithme de vérification

Complexité théorique :
 Temporelle : O(f(n))
( N 2−N )
2
[
F ( n )= ( 1 ) + N + 2
2 ] + N +1+1+ N + N +1+ N 2 + N

7 N 2−7 N +8
F ( n )= [ 2 ]
 T(N) = O(n2)

Le nombre d’itération est égale a


1+ N²+ (N²-N)/2)+ N²+1+1+n+n+1+ n²+n
Donc la complexité temporelle= O(N2) tel que n est le nombre de nœuds

 Spatiale :
La plus grande quantité d'espace dans cet algorithme de graphe est prise par la matrice
adjacente qui est une matrice à deux dimensions N*N, où n est le nombre de nœuds. La
complexité spatiale est donc O(N2).

Présentation d’une instance du problème (un exemple) :


Figure 8 Présentation d’une instance du problème
Exemple : Sabrina a la liste de courses suivante :
Animalerie (le chat noir a besoin d'un nouveau bac à litière) (1)
Pharmacie (aile de chauve-souris, dentifrice) (2)
Nettoyeur de vêtements (ramasser la robe noire chez les nettoyeurs) (3)
Dans quel ordre doit-elle faire ces courses afin de minimiser le temps passé sur son balai ?
- Temps entre chaque paire d'emplacements (minutes) :
La Maison (0) Animalerie(1) Pharmacie(2) Nettoyeur(3)
La Maison (0) 0 20 10 12
Animalerie(1) 20 0 15 11
Pharmacie(2) 10 15 0 17
Nettoyeur(3) 12 11 17 0
Le nombre de sommets est N = 4, donc le nombre de circuits Hamiltonien est
3!=3x2x1=6
Que diriez-vous de lister tous les circuits possibles ?
Circuits Hamiltonien Le temps:
0,1,2,3,0 64
0,1,3,2,0 58
0,2,1,3,0 48
0,2,3,1,0 58
0,3,1,2,0 48
0,3,2,1,0 64

Ce que nous venons de faire est l'algorithme Brute-Force :


- Je fais une liste de tous les circuits Hamilton possibles.
- Calculer le poids de chaque circuit Hamilton en additionnant les poids de ses arêtes.
- Choisissez le circuit Hamilton avec le poids total le plus petit.
Dans cet exemple le temps minimal est 48.
Etude Expérimentale : (Variation de la taille du problème) :
Simulation de la complexité théorique de l’algorithme de résolution :
Complexité temporelle :
Pour calculer la complexité temporelle on a utilisé une insertion automatique ci-dessous
(aléatoirement) de la matrice d’adjacence.

int z=0,j;
int first_node,second_node,weight;
while(z<num_nodes-1)
{
j=z+1;
while(j<num_nodes)
{
first_node=z;
second_node=j;
weight=(rand()%15)+1;
edges_list[first_node][second_node] = weight;
edges_list[second_node][first_node] = weight;
j++;
}
z++;
}

Figure 9 pseudo code pour l'insertion auto

La complexité temporelle de l'algorithme dépend du nombre de nœuds (N) donc on va varier


la valeur de N

N Complexité temporelle
4 0.001s
6 0.001s
8 0.002s
10 0.149s
11 1.389s
12 15.254s
13 251.94s
14 Environ 2 heures
15 Plus qu'une journée
20 Plus d'un million d'années
complexité théorique de l’algorithme de résolution
300

250
temps d'execution en Secondes

200

150

100

50

0
4 6 8 10 11 12 13
nombres de noeuds
Complexité spatiale :
L’espace mémoire dans cet algorithme de graphe est prise par la matrice d’adjacence qui est
une matrice à deux dimensions N*N, où n est le nombre de nœuds. La complexité spatiale est
donc O(N2).

N Complexité spatiale
4 16 octets
6 36 octets
8 64 octets
10 100 octets
11 121 octets
12 144 octets
13 169 octets
14 196 octets
16 256 octets
18 324 octets
20 400 octets

complexité théorique de l’algorithme de résolution

450
400
350
taille memoire en octets

300
250
200
150
100
50
0
4 6 8 10 12 14 16 18 20
N

Simulation de la complexité théorique de l’algorithme de vérification :


Complexité temporelle :
La complexité temporelle de l'algorithme dépend du nombre de nœuds (N) donc on va varier
la valeur de N

N Complexité temporelle
4 0.008s
6 0.02s
10 0.034s
12 0.046s
20 0.146s
40 0.476s
60 1.359s

complexité théorique de l’algorithme de verification

0.03
temps d'execution en Secondes

0.02

0.02

0.01

0.01

0
4 6 8 10 12 14 16
N

Complexité spatiale :
L’espace mémoire dans cet algorithme de graphe est prise par la matrice d’adjacence qui est
une matrice à deux dimensions N*N, où n est le nombre de nœuds. La complexité spatiale est
donc O(N2).
Donc c’est les mêmes résultats que l’algorithme précèdent.

Conclusion :
L'algorithme Brute-Force est une technique qui garantit des solutions aux problèmes de
n'importe quel domaine aide à résoudre les problèmes les plus simples et fournit également
une solution qui peut servir de référence pour évaluer d'autres techniques de conception
C’est un algorithme optimal mais inefficace. Il est garanti de trouver une solution, mais cela
peut prendre un temps déraisonnable pour le faire.
L'algorithme de force brute est utile pour résoudre des problèmes de petite taille, il est souvent
plus facile à mettre en œuvre qu'un autre plus sophistiqué et en raison de cette simplicité, il
peut parfois être plus efficace et aussi en l’utilise lorsque la simplicité est plus importante que
la vitesse.
Par contre les autres algorithmes comme plus proche voisin est efficace mais non optimal.
Donc c'est rapide et facile, mais on ne trouve pas toujours le circuit Hamilton le plus léger.
Références :

Algorithmique Moderne Analyse et Complexité Auteur : Habiba Drias zerkaoui.


https://openclassrooms.com/
https://www.geeksforgeeks.org/
Annexe -code source- :

#include <bits/stdc++.h>

using namespace std;

class brute_force

public:

int shortest_path_sum(int** edges_list, int num_nodes)

int source = 0;

vector<int> nodes;

for(int i=0;i<num_nodes;i++)

if(i != source)

nodes.push_back(i);

int n = nodes.size();

int shortest_path = INT_MAX;

while(next_permutation(nodes.begin(),nodes.end()))

{
int path_weight = 0;

int j = source;

for (int i = 0; i < n; i++)

path_weight += edges_list[j][nodes[i]];

j = nodes[i];

path_weight += edges_list[j][source];

shortest_path = min(shortest_path, path_weight);

return shortest_path;

};

int main()

/// Getting the number of nodes and number of edges as input

int num_nodes,num_edges;

cout << "\n Donnez le nombre des noeuds puis le nombre d'aretes: ";

cin >> num_nodes >> num_edges; ///O(1)


/// creating a multi-dimensional array

int** edges_list = new int*[num_nodes];

for(int i=0;i<num_nodes;i++)

edges_list[i] = new int[num_nodes];

for(int j=0;j<num_nodes;j++)

edges_list[i][j] = 0; ///O(N²)

/// adjacent matrix filling mechanism

cout << "\n donnez l'aretes et leur poids exemple:\n noeud1 noeud2 poids\
n";

for(int i=0;i<num_edges;i++)

int first_node,second_node,weight;

cin >> first_node >> second_node >> weight; ///O((N²-N)/2)

edges_list[first_node][second_node] = weight;

edges_list[second_node][first_node] = weight;

}
int a[100],distance=0;

for(int i=0;i<num_nodes;i++)

for(int j=0;j<num_nodes;j++)

cout << edges_list[i][j] << " "; ///O(N²)

cout << endl;

cout << endl << endl;/*

///execution de l'algorithme de resolution

brute_force approach1;

cout << approach1.shortest_path_sum(edges_list,num_nodes) << endl;*/

///algorithme de verification

bool doubl=true;

int k,i=1;

//entrer la distance minimale

cout<<"entrer la distance minimale:"; ///O(1)

cin>>k; ///O(1)

//entrer le chemin

cout<<"donner le chemin:";

cin>>a[0];
do{

cin>>a[i]; ///O(n)

i++;

}while(a[i-1]!=a[0]);

//calcul de la distance

for(int j=0;j<i;j++){

distance+=edges_list[a[j]][a[(j+1)%i]]; ///O(n)

//tester si la distance est minimale ou non

if(distance<=k){ ///O(1)

cout<<"\nle chemin est "<<distance<<" il est optimale";}

else{cout<<"\nle chemin est "<<distance<<" il n'est pas optimale";}

//teste si un sommet est parcouru plus d'une fois

for(int l=1;l<i;l++){

for(int q=1;q<i;q++){

if(l!=q&&a[l]==a[q])doubl=false; ///O(n²)

//affichage des resultats des testes

if(i==num_nodes||!doubl) { ///O(n)

cout<<"\n mais tu ne pas parcouru toutes les sommets";}


else{

cout<<"\n toutes les sommets sont parcourus ";}

///end algorithm

return 0;
}
Figure 10 code source en C++

View publication stats

Vous aimerez peut-être aussi