Projet de GraphesAlgorithme de DijkstraMichaë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++.
Nous avons en fait implémenté trois classes :
Déclaration de ces classes :
/////////////////// classe sommet /////////////////////////////////
class sommet
/* chaque sommet contient une liste de successeur
et son numéro dans le graph.*/
{
unsigned int code;
//numéro du sommet
list_sommet * succ; //liste des successeurs
public:
//constructeurs et destructeur
sommet(){succ=NULL;}
sommet(unsigned int c):code(c) {succ=NULL;}
~sommet(){ supprime_liste();}
//suppression de la liste des successeurs de la
mémoire
void supprime_liste();
//ajout d'un successeur dans la liste du sommet
void ajout_liste(sommet * s,const unsigned int p);
//affichage du numéro du sommet ainsi que de ses
successeurs
void affiche() const;
//renvoie si le sommet a pour successeur le sommet
dont le numéro est passé en paramètre
int a_pour_succ(const unsigned
int c) const;
//partage des données avec la classe graph
friend class graph;
};
Voici un exemple illustrant cette structure de données :
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.
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
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é.
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 :
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.
Tous ces tests ont été effectués avec un K6-2 300 MHz et 128Mo de RAM.
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.
n = nombre de sommets
|
temps moyen pour l'algo de Dijkstra en millisec.
|
n² ramené aux valeurs des temps de calculs
|
n²
|
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 |
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