Boravy CHIN Baptiste DURAND E3 .. Rapport de PR-3602 Approfondissement en informatique Thème « Graphes et algorithmes » http://perso.esiee.fr/~durandb/ 1 Table des matières 0 - Introduction générale ............................................................................................ 3 I - Questions de préparation ....................................................................................... 4 II - Problèmes introductifs........................................................................................... 5 II - A - Problème d’affectation.................................................................................. 5 II - B - Problème de la carte routière ....................................................................... 7 III - Travail demandé .................................................................................................. 8 III – A – Enoncé ...................................................................................................... 8 III – B – Indications concernant la théorie ............................................................... 8 III – B – 1 – Le Graphe de résolution de problèmes ............................................ 9 III – B – 2 – Le Graphe de carte........................................................................... 9 III – C – Indications concernant la mise en pratique ............................................. 10 III – C – 1 – Le choix du langage JAVA ............................................................. 10 III – C – 2 – Le Graphe de résolution de problèmes .......................................... 10 III – C – 3 – Le Graphe de carte ........................................................................ 10 III – D – Heuristiques ............................................................................................ 11 III – D – 1 – Introduction .................................................................................... 11 III – D – 2 – Explication des différentes heuristiques ......................................... 11 III – D – 3 – Comparaison des différentes heuristiques ..................................... 12 III – D – 4 – Synthèse ........................................................................................ 13 III – E – Déroulement de notre programme ........................................................... 14 III – E – 1 – Déroulement ................................................................................... 14 III – E – 2 – Tests de débogage ......................................................................... 14 III – E – L’interface graphique ............................................................................... 15 III – E – 1 – Introduction ..................................................................................... 15 III – E – 2 – Principe d’utilisation ........................................................................ 15 IV – Liens utiles ........................................................................................................ 17 V – Remerciements .................................................................................................. 18 2 0 - Introduction générale Le but du projet PR-3602 est de réaliser proposer et d'implémenter une méthode de résolution au célèbre problème du voyageur de commerce. C'est un problème d'optimisation fort simple à comprendre, mais dont on n'a pour l'instant pas encore trouvé de solution de résolution en temps de complexité polynomial. Qu'est-ce que le problème du voyageur de commerce ? Le problème du voyageur de commerce consiste en un voyageur (de commerce) qui souhaite parcourir un certain nombre de villes de manière à minimiser la distance totale parcourue et revenant au point de départ. Le problème revient donc à trouver le plus court-circuit hamiltonien. Pour ce faire, nous utilisons une implémentation commentée et expliquée de l'algorithme A*, un algorithme informatique constituant une approche très utilisée notamment dans l'intelligence artificielle et qui nous permet de résoudre ce problème en un temps non factoriel. Nous coderons cet algorithme en langage JAVA pour plus de commodité en évitant ainsi les différents problèmes des langages de bas-niveaux (problèmes de mémoire entre autres), tout en permettant la programmation orientée objet et l'utilisation de classes toutes faites très intéressantes pour la gestion des tableaux à taille variable (ArrayList). Ce projet utilise quatre classes : •Starter pour démarrer le projet •Node le noeud du Graphe de résolution de problème •Graph pour générer et afficher le graphe de carte sous forme de matrice carrée des distances •GUI notre interface graphique Le site web du projet est accessible à l’adresse indiquée en fin de rapport. 3 I - Questions de préparation Qu'est-ce qu'un Graphe de Résolution de Problème (GRP), relativement à un problème donné ? UN GRP est un graphe où : ● c’est un graphe connexe et sans cycle : une arborescence ● les sommets sont les états possibles du problème ● on distingue l’état initial et les états finaux ● le coût c(u) associé à l’arc u = (i, j) représente une règle permettant de passer de l’état i à l’état j ● le chemin de l’état initial à un état final constitue une solution au problème Quel GRP proposeriez-vous pour le problème du Voyageur de Commerce ? Pour le problème du Voyageur de Commerce, nous proposerions un GRP dont les nœuds sont les villes parcourues. Quel est, schématiquement, le fonctionnement d'un algorithme A* ? 1. On initialise une liste OUVERTE on y ajoute le nœud initial et une liste FERMEE vide. 2. On prend le 1er élément de la liste OUVERTE et on le met dans la liste FERME. 3. On rentre dans la grande boucle du programme A*. 4. On développe le dernier nœud de la liste FERMEE (celui que l’on vient d’ajouter dans cette liste à l’instant, c’est-à-dire que l’on ajoute ses successeurs potentiels du GRP dans la liste OUVERTE) 5. On prend le nœud de la liste OUVERTE qui minimise la fonction d’évaluation F = G + H et on l’ajoute à la liste FERMEE. 6. Si le nœud ajouté à la liste fermée est le nœud final (correspond à la condition de fin de la boucle), on continue en 7, sinon, on reboucle en 3. 7. On termine l’algorithme. Ainsi, la liste OUVERT représente l’ensemble des nœuds étudiés et la liste FERME représente l’ensemble des nœuds ayant été considérés comme faisant partie du chemin solution mais qui n’en font pas partie forcément. Que représentent les symboles g, h et f dans l'algorithme ? Soit n, un sommet pris au hasard dans le GRP, dans l’algorithme, on a : ● Le symbole g représente le coût du sommet initial au sommet n, ● Le symbole h représente la fonction heuristique choisie, ● Le symbole f représente la fonction d’évaluation définie par f(n) = g(n) + h(n) Quelle est la condition sur h pour que l'on parle d'algorithme A* ? On parle d’algorithme A* si, pour tout sommet n, h(n) ⪯ h(n*) donc que l’algorithme A trouve un chemin optimal du sommet initial à un but s’il en existe. 4 II - Problèmes introductifs II - A - Problème d’affectation On désire affecter n personnes à n travaux, chaque personne devant effectuer un travail et un seul. Le coût d’affectation de la personne i au travail j est d ij (il peut s’agir, par exemple, d’un coût de formation). On cherche à affecter les personnes aux travaux de sorte que la somme des coûts soit minimale. Proposez un graphe de résolution pour le problème de l’affectation. Quelle est la complexité de calcul de l’algorithme qui analyse toutes les solutions possibles ? La complexité de calcul de l’algorithme qui analyse toutes les solutions possibles est O(n!). 5 Proposez un algorithme A* pour résoudre le problème de l’affectation. Appliquer cet algorithme dans le cas où la matrice dij est donnée par : Nous avons choisi comme heuristique la somme des minimums des lignes de la matrice même s’ils étaient sur la même colonne ce qui aurait signifié que l’on affecte plusieurs personnes à une même tâche, or ce n’est pas possible d’après l’énoncé du problème. Proposer un autre graphe de résolution. Discuter de l’intérêt de cette nouvelle formulation. On peut en général associer plusieurs GRP à un même problème et l’un d’eux peut permettre limiter le plus possible la partie explorée par la recherche heuristique mais qui permet de trouver malgré tout le même résultat. 6 II - B - Problème de la carte routière On cherche à résoudre de façon automatique le problème suivant : étant donnée une carte routière, deux villes A et B, trouvez la route la plus courte (en nombre de kilomètres) entre A et B. Trouvez une bonne heuristique pour résoudre ce problème en utilisant un algorithme A*. Spécifier de façon précise les données qui sont nécessaires à l’algorithme. Pour résoudre ce problème, on pourrait utiliser l’algorithme de Dijkstra avec la fonction nulle comme fonction heuristique. Cependant, il est possible d’avoir une plus grande optimalité avec une fonction heuristique minorante et monotone, comme par exemple le vol d’oiseau, par rapport à la distance réelle d’une route. Indiquer le fonctionnement de l’algorithme sur un exemple simple. A partir du graphe ci-dessus, nous obtenons en utilisant l’algorithme de Dijkstra avec l’heuristique nulle le graphe suivant : 7 III - Travail demandé III – A – Enoncé Votre objectif sera d'implémenter la stratégie A* pour résoudre le problème du voyageur de commerce. Vous proposerez et implémenterez plusieurs heuristiques A*, et vous les testerez sur des cas générés manuellement (pour les plus petits) ou aléatoirement. Les heuristiques seront évaluées empiriquement en fonction du critère suivant : le rapport entre le nombre de nœuds développés par l'algorithme et la taille totale de l'arborescence de recherche. Vous utiliserez le langage de programmation de votre choix. Voici pour vous aider, à titre d'exemple, une trame pour une implémentation en langage C. Elle comprend : ● le fichier vdc.h, qui contient une structure de données pour représenter le Graphe de Recherche, et plus précisément les éléments de la liste OUVERT ; ● le fichier vdc.c, qui contient un découpage du programme en fonctions plus ou moins élémentaires à programmer. Les commentaires servent à spécifier le rôle de chaque fonction ou champ de structure, ainsi qu'à générer automatiquement une documentation du programme grâce au logiciel Doxygen. III – B – Indications concernant la théorie IMPORTANT : Nous ne chercherons pas la solution optimale au problème. Nous chercherons seulement à développer les nœuds de manière à obtenir un chemin de taille égale à celle du graphe qui peut se révéler être de qualité et proche de l’optimalité pour toutes les heuristiques sauf celle gloutonne. Ce qui explique que les différentes heuristiques ne trouvent pas forcément le même chemin hamiltonien. Cela permet diminuer le temps de calcul. Sachez cependant que l’heuristique nulle garantis l’optimalité absolue jusqu’au nœud n-1 (celui empreinte avant de revenir au point de départ). Pour obtenir la solution optimale à chaque fois, il faudrait faire tourner l’algorithme suffisamment longtemps (modifier uniquement la condition d’arrêt). Nous avons comme données la liste des villes et la matrice des distances entre ces villes. Nous commencerons par choisir quelques données particulières aux problèmes afin de trouver une solution générale. Nous utilisons ici deux graphes : le graphe de résolution de problèmes le graphe de carte. 8 III – B – 1 – Le Graphe de résolution de problèmes Le GRP est une arborescence servant à résoudre un problème donnée. Il est composé d’une liste OUVERTE représentant les nœuds analysés mais par encore rajoutés à la liste FERMEE. Et d’une liste FERMEE composée de nœuds extraits nécessairement de la liste OUVERTE. L’élaboration de la solution au problème va s’effectuer à partir de la liste FERMEE depuis le nœud terminant l’exécution de l’algorithme. Dans le GRP, un nœud ajouté à la liste FERMEE depuis la liste OUVERTE est retiré de la liste OUVERTE et ses successeurs depuis le graphe de carte, sont développés, c’est à dire ajoutés à la liste OUVERTE. Le GRP comporte des nœuds qui possèdent divers attributs tels que les chemins pour les atteindre, leur numéros, leur cout G, leur heuristique H et leur fonction d’évaluation F qui est égale à la somme de G et H. Il est possible d’estimer la taille du GRP. III – B – 2 – Le Graphe de carte Nous représenterons le graphe de carte sous forme de matrice dans notre programme informatique (cf : III – C – 2). Le graphe de carte est un graphe possédant propriétés suivantes : Il est connexe car il faut que le voyageur de commerce puisse circuler dans toutes les villes → La matrice le représentant n'aura donc aucune ligne ni aucune colonne entièrement nulle. Il est complet de manière à ce que le voyageur de commerce puisse passer dans chaque ville de manière indépendante → La matrice le représentant n'aura donc aucuns zéro hormis ceux de sa diagonale Il est non orienté car on considère que les routes reliant les différentes villes sont à double-sens → La matrice le représentant sera donc symétrique par rapport à sa diagonale Il est antiréflexif, c'est à dire que la distance d'une ville à elle-même est nulle → La matrice le représentant aura donc une diagonale nulle Comme le graphe des villes est un graphe complet, nous allons devoir choisir une ville de départ pour le programme mais qui, concrètement, ne comptera pas car le voyageur doit revenir à la ville de départ. Voici un exemple de correspondance entre un graphe complet et sa matrice qui décrit les distances entre les sommets : 9 III – C – Indications concernant la mise en pratique III – C – 1 – Le choix du langage JAVA Nous avons choisi de programmer notre programme en JAVA pour plusieurs raisons : La gestion simple de la mémoire qui évite ainsi l’utilisation de fonctions de gestions de mémoire telles que les pointeurs ou les allocations de mémoire manuelles La possibilité d’utiliser la programmation orienté objet, ce qui nous permet de créer des objets de type « nœud » ou de type « graphe » et donc de nous faciliter la représentation des entités La possibilité d’utiliser des tableaux à taille variable : les ArrayList La simplicité de programmation et le fait d’avoir des outils de gestions des ArrayList préprogrammées tels que les littérateurs de liste III – C – 2 – Le Graphe de résolution de problèmes Nous avons choisi de toujours partir du sommet 1 du graphe de carte dans le GRP pour des facilités de programmation. Pour représenter les différents nœuds du GRP, c’est-à-dire les nœuds des listes OUVERTE et FERMEE, nous créés une classe dédiée : Node. La classe Node contiendra aussi toutes les méthodes permettant de gérer le GRP tel que le développement d’un nœud, l’extraction d’un nœud, la sélection d’un noeud … Nous avons choisi de représenter les listes OUVERTE et FERMEE avec des objets de type ArrayList. Les ArrayList sont des tableaux à taille variable que l’on peut facilement étendre et qui possède de nombreuses méthodes préprogrammée pour les parcourir ou ajouter des objets. Les ArrayList des deux listes seront donc des listes de nœuds. III – C – 3 – Le Graphe de carte Nous avons choisi de prendre des distances entières entre chaque ville comprises entre 1 et 10. Si la distance est égale à 0 alors le chemin est trivial, c’est-à-dire qu’il correspond au chemin d’une ville à elle-même. Le chemin trivial n’est pas pris en compte par notre algorithme. Nous avons choisi de représenter le graphe des distances entre les villes sous la forme d’une matrice carrée symétrique par rapport à sa diagonale en tant que tableau 10 à deux dimensions pour des facilités de représentation et de programmation. (cf : III – B – 2) Pour simplifier la programmation, nous avons créé une classe dédiée pour représenter ce graphe de carte : la classe Graph. III – D – Heuristiques III – D – 1 – Introduction Le problème du voyageur de commerce peut être résolut de plusieurs manières avec des taux de fiabilité et de rapidité pouvant varier sensiblement. Ces différentes façons de procéder dépendent essentiellement de l’heuristique choisie. Pour notre programme, nous avons choisi trois heuristiques : Heuristique nulle Heuristique minimums restants Heuristique gloutonne Heuristique nulle améliorée III – D – 2 – Explication des différentes heuristiques Des exemples d’exécutions graphiques des différentes heuristiques sont disponibles sur le site web via l’adresse : http://perso.esiee.fr/~durandb/ L’heuristique nulle est, comme son nom l’indique, une heuristique qui est systématiquement égale à 0. Concrètement, notre algorithme A* va à chaque fois s’efforcer de rechercher le plus court chemin depuis la ville de départ jusqu’à une ville v. Notre algorithme A* devient ainsi un algorithme de Dijkstra ce lui qui garantis d’obtenir à coup sûr le plus court chemin jusqu’à la valeur n-1 (ce n’est pas le cas au rang n). Cela a cependant le principal défaut de consommer beaucoup de ressources et de nécessiter un temps de calcul pouvant rapidement devenir inabordable (à partir d’une taille de graphe de 7 dans notre cas à nous). Dans notre algorithme, cela revient à parcourir la liste OUVERTE et à prendre le nœud de coût G minimal. L’heuristique des minimums restants va elle en revanche utiliser une fonction d’heuristique égale à la somme des minimums des villes du graphe de carte encore non parcourues depuis un nœud du GRO donné. Si dans notre cas à nous, cela ne garantis pas à coup sûr d’obtenir la solution optimale au problème, cela représente néanmoins une solution convenable pour le problème, c’est-à-dire une solution de 11 qualité qui se rapproche de l’optimalité sans pour autant l’atteindre. De plus, cette heuristique permet d’économiser des ressources et du temps de calcul. L’heuristique gloutonne est une heuristique qui va se contenter de rajouter à la liste FERMEE, à chaque tour de boucle, le nœud minimisant la valeur de l’arc entre un nœud non compris dans la liste FERMEE nœud et le dernier nœud rajouté à la liste FERMEE. Elle ne prend à chaque fois en considération que le minimum local. Ce qui signifie qu’elle peut trouver, notamment pour les graphes de carte de grande taille, une solution au problème inconvenable, c’est-à-dire très éloignée de l’optimalité. L’heuristique nulle améliorée ressemble à l’heuristique nulle à la seule différence qu’elle prend en compte la longueur du chemin (le nombre de sommets parcourue jusqu’au nœud). Dans notre cas à nous, on utilise la valeur 100-longeur du chemin, valeur qui diminue quand le chemin s’allonge. Elle permet, quand deux nœuds du GRP ont le même coût minimum de la liste OUVERTE, de choisir le nœud ayant le chemin le plus long contrairement à l’heuristique nulle qui prendrai le systématiquement le premier nœud de la liste ouverte. III – D – 3 – Comparaison des différentes heuristiques Lors des différentes exécutions des algorithmes, nous avons pu mesurer un certain nombre de différences entre les différentes heuristiques : L’heuristique nulle avec l’algorithme de Dijkstra met toujours beaucoup plus de temps à s’exécuter que les autres heuristiques. Les temps de calcul explosent à partir d’une taille de 7 mais elle garantit en revanche de trouver la solution optimale au problème jusqu’à un rang taille du graphe de carte -1. Si la condition de fin d’algorithme A* garantissait l’optimalité de la solution, ce serai une heuristique intéressante en terme de temps de calcul. L’heuristique gloutonne s’exécute en un temps proportionnel à la longueur du graphe de carte et est donc de loin l’heuristique la plus rapide de toutes. En revanche, il lui arrive souvent de trouver une solution très éloignée de la solution optimale. Si la condition de fin d’algorithme A* garantissait l’optimalité de la solution, ce ne serai pas une heuristique intéressante en terme de temps de calcul car elle ferait développer beaucoup de nœuds dans le GRP. L’heuristique des minimums restants est à mi-chemin entre les deux précédentes heuristiques. Elle possède une rapidité inférieure à l’heuristique gloutonne, mais largement supérieure à celle de l’heuristique nulle. Elle trouve des solutions de plus grande taille que l’heuristique nulle, mais celles-ci restent tout à fait convenables contrairement à celles obtenues par l’heuristique gloutonne. Si la condition de fin d’algorithme A* garantissait l’optimalité de la 12 solution, ce ne serai l’heuristique la plus intéressante en terme de temps de calcul car elle minimiserait le nombre de nœuds développés. L’heuristique nulle améliorée est légèrement plus rapide que l’heuristique nulle car elle prend en compte la taille du chemin (pas le cout) ce qui réduit le nombre de nœuds développés. Si la condition de fin d’algorithme A* garantissait l’optimalité de la solution, ce serai une heuristique assez peu intéressante en terme de temps de calcul. III – D – 4 – Synthèse Au regard des différents constats effectués précédemment, il apparaît clairement que ni l’heuristique nulle, ni l’heuristique gloutonne, ni la nulle améliorée ne sont vraiment adaptés pour résoudre le problème quand le graphe de carte dépasse une certaine taille. Diverses raisons de temps de calcul ou d’optimalité de la solution trouvée nuisent à leur mise en pratique. L’heuristique des minimums restants semble être un bon compromis entre ces trois heuristiques car elle possède un temps de calcul tout à fait raisonnable pour des graphes de grande taille tout en donnant une solution convenable au problème. C’est donc plutôt à partir de celle-ci que nous recommandons l’exécution de l’algorithme. Elle serait même la meilleure des heuristiques en termes de performances (nombre de nœuds du GRP développés …) si l’on avait une condition finale garantissant l’optimalité de la solution. Tableau synthétique selon la condition de fin : chemin de taille égale à celle du graphe de carte : Nom heuristique Nulle Min restants Gloutonne Nulle + Temps de calcul X ~ V ~ Optimalité de la solution V (→ n-1) ~ X ~ Efficacité (grands graphes) X V X X 13 III – E – Déroulement de notre programme III – E – 1 – Déroulement L’appui sur le bouton de lancement permet d’exécuter l’algorithme. Il démarre et boucle tant que la condition de fin n’est pas vérifiée. III – E – 2 – Tests de débogage Les tests de débogage sont inclus dans l’interface graphique. Leur utilisation s’effectue comme ceci (façon que nous avons utilisée pour déboguer notre code) : Pour comparer les différentes heuristiques entre elles : Sélectionner une taille du graphe comprise entre 4 et 7 Sélectionner l’indicateur de conservation de graphe pour éviter de régénérer un graphe aléatoire à chaque fois. Sélectionner l’affichage de la liste FERMEE Lancer l’algorithme à plusieurs reprises (environ 10 fois) par heuristique choisie Regardez le déroulement de l’affichage des solutions et accessoirement le déroulement de l’algorithme. Vous pouvez évaluer les performances de l’heuristique en comparant le nombre de nœuds développées à la taille du GRP (si vous avez sélectionné l’option d’affichage), le temps d’exécution (ms) et le nombre de tours de l’algorithme. Pour changer de graphe de carte, désélectionner l’indicateur de conservation de graphe, lancer l’algorithme, resélectionner cet indicateur. Pour vérifier une heuristique en particulier : Regarder le code source disponible sur le site web Regarder les exemples d’exécution disponibles sur le site web Regarder la JAVADOC du projet disponible sur le site web. Sélectionner l’affichage des listes OUVERTE et FERMEE (ralentis le programme mais permet un affichage détaillé) Lancer une heuristique pour un graphe de 4 (facile à analyser visuellement) Analyser le déroulement du programme Notez que l’affichage des listes ralentis fortement la progression de l’algorithme. Il ne faut donc les afficher que lors du débogage. 14 III – E – L’interface graphique III – E – 1 – Introduction Pour plus de commodité, nous avons ajoutés une interface graphique de manière à rendre l’utilisation de cet algorithme plus simple et surtout plus intuitive. En effet, d’une part, notre algorithme peut ainsi être utilisé directement en exécutable au format .jar sans effectuer de manipulation particulière. D’autre part, cela permet de faciliter la lecture des différentes composantes (graphe généré de manière aléatoire, déroulement de l’algorithme, solution …). Enfin, l’utilisation d’une interface graphique permet de modifier le fonctionnement de l’algorithme (choix de l’heuristique par exemple), sans avoir à en modifier le code source du programme. III – E – 2 – Principe d’utilisation Notre interface graphique est composée comme ceci : 1. Un écran affiche en haut à gauche la matrice représentant les arcs des distances entres les villes (graphe de carte) 2. Un écran situé en bas à gauche affiche la solution de l’algorithme (nb : si l’algorithme réussis à la trouver cf : JAVADOC). Elle affiche aussi le temps d’exécution ainsi que le nombre de tours de l’algorithme A*. 3. L’écran de droite se charge d’afficher le déroulement de l’algorithme tel que les nœuds composants les différentes listes 4. La barre glissante en bas à gauche permet de choisir la taille du graphe de carte 5. Ce bouton permet de conserver le même graphe de carte entre deux exécutions consécutives (débogage) 6. Les boutons en bas au centre servent à choisir l’heuristique de calcul parmi les différents choix proposés 7. Les boutons en bas à droite permettent à l’utilisateur de choisir l’affichage des listes ouvertes ou fermées à sa guise (débogage) 8. Le bouton en bas à droite sert à exécuter l’algorithme selon les critères choisis par l’utilisateur L’interface graphique utilise des bibliothèques incluses par défaut lors du téléchargement du JDK. Une version à jour de JAVA est cependant fortement recommandée. L’interface graphique permet donc un débogage aisé du programme A*. 15 16 IV – Liens utiles La JAVADOC, le code source, le programme en JAVA, le rapport PDF et les exemples d’exécution sont disponibles sur le site web de notre projet à l’adresse suivante : http://perso.esiee.fr/~durandb/ Lien vers le site web de M.Couprie : http://perso.esiee.fr/~coupriem/PR3602/ 17 V – Remerciements Nous tenons à remercier M.Couprie pour nous avoir aidé, encadré et conseillé dans notre travail sur l’algorithme A*. 18