Projet de Graphes

Algorithme de Dijkstra

Michaël Ortega & Nicolas Dumoulin

Le but de ce projet est de programmer l'algorithme de Dijkstra pour déterminer un plus court chemin (il peut y en avoir plusieurs, de même longueur) dans un graphe orienté muni de distances non négatives. Nous avons choisi de coder ce programme en C++.


Structure de données utilisée :

Nous avons en fait implémenté trois classes :

Déclaration de ces classes :

Voici un exemple illustrant cette structure de données :

schéma explicatif de la structure de données


Algorithme :

Soit un graphe de n sommets. On considère que ce graphe ne contient pas de circuit négatif (c'est-à-dire, qu'il ne contient pas d'arc ayant un poids négatif). On appelle Pi(x) la plus courte distance entre le sommet source s (dans notre cas ce sera le sommet numéro 0) et le sommet x. On appelle distance la somme des arcs parcourus pour aller d'un sommet à un autre.

Variables utilisées :

Initialisation :

Parcours du graphe et calcul du plus court chemin :

Tant que il existe un i tel que S(i)=0 ( tant qu'il existe un sommet non marqué dans le graphe de départ )
et que Pi(xpivot)<infini
Faire
courant=tabsommet[xpivot].succ ( on initialise le pointeur courant sur le premier successeur de xpivot )
déclaration d'un entier x
Tant que courant != NULL faire
x=code du sommet successeur
Si Pi(x) > Pi(xpivot) + *courant.poids alors Pi(x)=Pi(xpivot) + *courant.poids
Fin Tantque
déclaration d'un entier min (pour le stockage du Pi minimum )
Pour tout sommet i Si S(i)=0 ( s'il le sommet n'est pas marqué )
alors Si Pi(i)<min alors min=Pi(i) et xpivot=i
Finsi
Fin Pour
S(xpivot)=1 ( marquage du sommet pivot )
Pour tout i : sommet marqué faire
courant=tabsommet[i].succ ( on initialise le pointeur courant sur le premier successeur de i )
Tant que courant != NULL et code du courant!= xpivot faire courant=courant.suivant
Si (courant!=NULL) alors
Si (Pi[xpivot]=(Pi[i] + courant->poids)) alors ajout de l'arc (i,xpivot) dans graph
Finsi
Fin Pour
Fin Tantque


Réalisation du programme :

Le programme se compose de trois fichiers :

Pour télécharger les sources et le makefile , choisissez le format de l'archive :

!!! Important !!! Pour compiler les sources, si vous êtes sous visual C++, vous devez avoir au début du fichier graph.cpp ceci :
#define visualcpp
//#define autres_compilos

Sinon, si vous êtes avec Borland C++ ou g++, vou devez inversez les commentaires, pour avoir :
//#define visualcpp
#define autres_compilos

Pour plus d'explication, jetez un coup d'oeuil dans les sources, c'est expliqué!

Pour faire plus simple, vous pouvez télécharger l'exécutable pour Windows : executable

Pour en savoir plus sur le contenu des classes et les procédures et fonctions, consultez le listing commenté.

Vérification de la connexité du graphe :

(ou comment ne pas faire tourner un programme en rond)

En effet, si le graphe n'est pas connexe, notre algorithme "tourne en boucle". Pour remédier à cela, on a créé la fonction grap::connexe() qui :

A propos de la génération aléatoire :

Nous avons dévellopez une fonction pour générer aléatoirement des graphes.

Cette fonction prend deux paramètres : le nombre de sommets que le graphe doit contenir et le pourcentage de chance que deux sommets soit relié par un arc. Pour cela on parcours simplement le graphe, et pour chaque sommet i, on parcours à nouveau le graphe, excépté le sommet i, et pour chaque sommet j, on tire un nombre aléatoire compris entre 0 et 100, et si ce nombre est inférieur au coefficient passé en paramètre, alors on crée un arc entre le sommet i et le sommet j avec un poids tiré aléatoirement. Puis on vérifie que le graphe trouvé est bien connexe, le cas échéant, on le rend connexe avec la fonction cité plus haut.


Résultats obtenus :

Tous ces tests ont été effectués avec un K6-2 300 MHz et 128Mo de RAM.

Dans ce premier tableau, figurent les résultats d'une série de tests effectués pour des graphes de 100, 150, 200, 300, 400, 500, 600, 700, 800, 900 et 1000 sommets, qui ont été générés aléatoirement. Pour chaque quantité de sommets, nous avons effectués cinq tests pour ensuite en faire la moyenne. En effet, étant donné que les graphes sont générés aléatoirement, les résultats varient d'un garphe à l'autre. En faisant cinq, nous pensons que les résultats se rapprochent plus de la réalité.
nombre de sommets
temps 1
temps 2
temps 3
temps 4
temps 5
100 0 0 0 60 0
150 0 50 0 0 50
200 110 610 600 50 110
300 720 880 830 830 770
400 1100 1100 1150 1150 110
500 1710 1710 1700 1700 1650
600 2860 2740 3080 3790 3410
700 4890 4720 4890 3950 4830
800 6810 7750 6920 7580 6260
900 9940 9330 11260 9940 9720
1000 14450 13950 15000 14670 13840
Après avoir calculer les moyennes de temps, nous nous sommes interressé à comment pouvoir les comparer avec la complexité théorique qui est, rappelons le en o(n²). Nous avons donc, tout d'abord calculé la valeur de n² pour chaque quantité de sommet. Puis nous avons décidé qu'il fallait que ces courbes aient en commun deux points le premier (l'origine 0) et le dernier. Donc pour 1000 sommets, nous retrouvons les même valeurs pour le temps d'exécution et la courbe théorique. Pour les autres quantité de sommets il suffit de diviser la valeur de n² corrsepondante par 1000000 puis de la multiplier par 14382. Ainsi, par exemple, pour 600 sommets on trouve (360000/1000000)*14382=5177.52.
Ainsi, on peut comparer graphiquement les deux complexités.
n = nombre de sommets
temps moyen pour l'algo de Dijkstra en millisec.
n² ramené aux valeurs des temps de calculs
100
12
143,82
10000
150
20
323,595
22500
200
296
575,28
40000
300
806
1294,38
90000
400
922
2301,12
160000
500
1694
3595,5
250000
600
3176
5177,52
360000
700
4656
7047,18
490000
800
7064
9204,48
640000
900
10038
11649,42
810000
1000
14382
14382
1000000

complexité pratique complexité théorique

Comparaison complexité pratique / théorique :

Interprétation des résultats :

Nous allons baser notre analyse sur la courbe ci dessus. Cette courbe représente la complexité théorique ainsi que la complexité pratique. A première vue, les courbes ont le même profil. On remarque nettement que la complexité pratique est inférieure à la complexité théorique, ce qui se justifie par le fait que la complexité théorique se base sur le pire des cas, et ce cas là est rarement atteint, voire impossible avec une génération aléatoire.


dernière mise à jour : mardi 15 mai 2001
Licence d'informatique - Université Blaise Pascal - Clermont-Ferrand