TD10 : Plus courts chemins

 Login :  Mot de passe :

Itinéraire calculé par Google Maps, entre le Metropolitan Museum of Art et le Intrepid Sea, Air & Space Museum (New York City)

Avant de commencer

Testez les classes fournies avec le fichier Test0.java.

Vous devez voir s’afficher la carte de Manhattan et la sortie suivante :

Test 0 : test de la classe Graph
Loading road networks from file mini.gr ...  done
Loading road networks from file USA-road-d-NY.gr ...  done
Loading geometric coordinates from file USA-road-d-NY.co ...  done
Loading background image from file NY_Metropolitan.jpg ...  done

Introduction

Nous nous intéressons aujourd’hui au calcul d’un plus court chemin dans un graphe. Lorsque le graphe est un peu gros, comme lors du calcul d’un itinéraire routier, il faut utiliser des algorithmes performants pour obtenir le résultat en un temps acceptable.

Nous allons implémenter le plus simple et le plus classique des algorithmes efficaces pour ce problème, l’algorithme de Dijkstra. Cet algorithme s’applique à des graphes dont les poids des arêtes sont positifs.

La classe Graph

On vous fournit une classe Graph pour représenter des graphes orientés, de sommets 0, 1, ..., nbVertices-1, et où les poids des arcs sont des entiers strictement positifs.

Ses principaux champs et méthodes utiles dans la suite sont les suivants :

La classe Dijkstra

La classe Dijkstra fournie contient les champs suivants :

Le squelette contient un constructeur standard Dijkstra(Graph g, int source, int dest).

Tout sommet du graphe peut servir de source ou de dest. En particulier, aucun de ces deux sommets n’est forcément le sommet 0.

Rappel de l’algorithme de Dijkstra

Illustration de l’exécution de l’algorithme de Dijkstra

L’algorithme de Dijkstra peut être vu comme une généralisation du parcours en largeur d’un graphe au cas où les arcs sont pondérés mais de poids positifs.

Il fait intervenir

Initialement :

Tant qu’il reste des sommets à examiner (i.e. W n’est pas vide) :

À la fin :

1 Au boulot !

1.1 Les bases

Nous dirons qu’un sommet a été atteint lorsqu’au moins un chemin a été trouvé entre la source et ce sommet.

Ajoutez à la classe Dijkstra les champs suivants et modifiez le constructeur pour les initialiser convenablement :

Nous conviendrons qu’initialement la source est son propre prédécesseur.

Testez avec Test11.java.

Déposez le fichier Dijkstra.java.

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

1.2 L’ensemble W

Nous voulons pouvoir effectuer efficacement les opérations suivantes sur l’ensemble W des sommets atteints à traiter :

Nous utilisons pour cela une file de priorité de type PriorityQueue<Node> qui assure ces opérations en temps logarithmique en le nombre d’éléments de l’ensemble. La classe Node utilisée pour représenter les paires (sommet, distance) est fournie. Les méthodes dont nous aurons besoin sur les files de priorité sont :

Il y a une subtilité : les priorités des sommets peuvent être amenées à changer, (un sommet déjà atteint une première fois peut l’être à nouveau atteint par un chemin plus court), or la classe PriorityQueue n’offre pas de moyen efficace de mettre à jour la priorité d’un élément.

Pour contourner ce problème, lorsque la priorité d’un sommet i contenu dans l’ensemble W doit être modifiée, nous insérerons dans W une nouvelle copie de i avec la nouvelle priorité. Cette nouvelle occurrence de i sortira forcément de W avant l’ancienne. En contrepartie, il se pourra qu’un sommet tiré de W soit en réalité déjà définitivement traité par l’algorithme et il y aura donc une vérification à faire quand on extrait un sommet.

Ajoutez à la classe Dijkstra un champ unsettled représentant l’ensemble W. Modifiez le constructeur de la classe Dijstra pour initialiser correctement unsettled et y insérer la source.

Testez avec Test12.java.

Déposez le fichier Dijkstra.java

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

2 Dijkstra unidirectionnel

Nous allons maintenant implémenter l’algorithme de Dijkstra en partant des petites fonctionnalités avant d’assembler le tout. (Cette manière de procéder nous permettra de réutiliser ensuite la méthode oneStep() isolément.)

Complétez la méthode void update(int succ, int current) qui effectue les mises à jour sur le successeur succ d’un sommet current pioché dans W. En particulier, si dist[succ] change, on mettra aussi à jour le prédécesseur du sommet succ, et on insérera le sommet succ avec sa nouvelle priorité dans la file unsettled.

Complétez la méthode int nextNode() qui effectue le choix dans W du sommet current, avec la plus petite valeur dist[current]. Il s’agit de retirer de unsettled des sommets dans l’ordre de leur priorité, jusqu’à rencontrer un sommet non encore traité et le renvoyer. La méthode doit renvoyer -1 s’il n’a a pas de sommet atteint non traité.

Complétez la méthode int oneStep() qui réalise une itération de la boucle principale de l’algorithme de Dijkstra : extraire de W le prochain sommet current à traiter, renvoyer -1 s’il ne reste aucun sommet à traiter, marquer current comme traité, mettre à jour tous les successeurs de current. La méthode renverra le sommet current qui vient d’être traité.

Enfin, complétez la méthode int compute() qui implémente l’algorithme de Dijkstra à partir des briques précédentes et renvoie la longueur d’un plus court chemin de source à dest.

Testez en utilisant Test2.java. Vous devez obtenir :

Test 2 : test de l'algorithme de Dijkstra unidirectionnel utilisant le graphe dans 'mini.gr'...
*** ATTENTION : dans l'affichage, la numérotation des nœeuds commence à 0 ***
***             (et non à 1 comme dans 'mini.gr')                         ***
Loading road networks from file data/mini.gr... done
Test de la methode update()...  [OK]
Test de la methode nextNode...  [OK]
Test de la methode oneStep...   [OK]
Test de la methode compute...   [OK]

Maintenant, pour visualiser le résultat, il faut :

Exécutez la classe Test2bis dans le fichier Test2bis.java. Vous devez voir avancer votre ensemble de points traités (en rouge) et les points de W (en vert).

Illustration du déroulement de l’algorithme de Dijkstra

À la fin, vous devez obtenir le plus court chemin suivant :

État final du déroulement

et vous devez voir s’afficher :

plus court chemin entre 190637 et 187333 = 39113

Déposez le fichier Dijkstra.java

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

3 Dijkstra bidirectionnel

Illustration de l’exécution de l’algorithme de Dijkstra bidirectionnel

À partir de maintenant, on suppose qu’il existe toujours au moins un chemin entre la source et la destination.

L’algorithme

Nous allons désormais utiliser l’algorithme de Dijkstra pour faire une recherche bidirectionnelle, qui sera généralement plus rapide sur des graphes issus de cartes comme ici.

De manière schématique, dans l’algorithme de Dijkstra, on fait grossir une boule centrée sur la source, et on s’arrête lorsque la destination est contenue dans cette boule. Dans un Dijkstra bidirectionnel, on fait grossir tour à tour deux boules, une centrée sur la source et une centrée sur la destination, jusqu’à ce que ces deux boules se touchent. Si le temps de calcul croît avec le volume des boules, on s’attend à ce que la deuxième méthode soit plus rapide.

Illustration de la différence entre Dijkstra uni- et bi-directionnel

3.1 Version naïve approchée

Nous allons commencer par une version naïve qui interrompt la recherche dès que les deux boules se touchent. Cela ne donne pas forcément le chemin le plus court, mais nous verrons ensuite comment corriger le problème.

Ouvrez le squelette de la classe BiDijkstra qui contient les champs suivants :

Complétez

Testez avec Test31.java. Vous devez obtenir :

Test 3.1 : test de l'algorithme de Dijkstra bidirectionnel naif

Loading road networks from file data/mini.gr... done
Test du constructeur BiDijkstra(Graph, int, int)... [OK]
Test de la methode flip()...                        [OK]
Test de la methode oneStep()...                     [OK]
Test de la methode isOver()...                      [OK]
Test de la methode compute()...                     [OK]
Test de la methode getMinPath()...                  [OK]

Loading road networks from file data/USA-road-d-NY.gr...    done
Loading geometric coordinates from file data/USA-road-d-NY.co ... done
Test de la methode getMinPath()...                  [OK]

Exécutez la classe Test31bis dans le fichier Test31bis.java. Vous devez voir avancer vos deux paquets de points traités (en rouge) et leurs frontières (en vert) :

Illustration du déroulement de l’algorithme de Dijkstra bidirectionnel naïf

et obtenir le chemin :

État final du déroulement

Sur ce chemin, le point last est le gros point bleu central. Dans la console, vous devriez obtenir :

Loading background image from file data/NY_Metropolitan.png ...  done
longueur chemin forward    entre 190637 et 187333 = 39113
longueur chemin backward   entre 190637 et 187333 = 39113
longueur chemin BiDijkstra entre 190637 et 187333 = 39113

Déposez le fichier BiDijkstra.java

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

3.2 Le calcul final du plus court chemin

Exécutez maintenant la classe Test32bis dans le fichier Test32bis.java, dans lequel nous avons changé le point source. Quel nouveau phénomène se produit par rapport à l’exemple précédent ?

Pour comprendre la faiblesse de la méthode naïve et comment la corriger, examinons le schéma suivant.

Illustration du raffinement du BiDijkstra

Dès que le sommet x a été traité par les deux recherches, nous avons un chemin entre la source et la destination : l’union des plus courts chemins obtenus entre la source et x et entre x et la destination. Il se peut que le plus court chemin ne passe pas par x et donc que le chemin trouvé ne soit pas le plus court. Lorsque cela se produit, il existe deux sommets i et j tels que

dist(source, i) + dist(j, dest) + weight(i, j) < dist(source, x) + dist(x, dest).

On peut démontrer le fait suivant (preuve plus bas) : soit x l’unique sommet tel que forward.settled[x] = backward.settled[x] = true. Soit a un des chemins les plus courts de source à dest, et supposons que

length(a) < forward.dist[x] + backward.dist[x].

Alors le dernier point i sur a (partant de source) tel que forward.settled[i] = true est dans backward.unsettled, et l’on a

length(a) = forward.dist[i] + backward.dist[i].

Modifiez votre méthode int getMinPath() afin qu’elle vérifie s’il existe un chemin plus court comme indiqué ci-dessus et si c’est le cas, mette à jour la variable last avant de renvoyer la longueur du chemin le plus court.

Testez avec Test32.java et à nouveau avec Test32bis. Vous devriez cette fois obtenir :

Test 3.2 : test de l'algorithme de Dijkstra bidirectionnel
Loading road networks from file data/USA-road-d-NY.gr...    done
Test de la methode getMinPath()...  [OK]

longueur chemin forward    entre 190637 et 187333 = 35478
longueur chemin backward   entre 190637 et 187333 = 35478
longueur chemin bidijkstra entre 190637 et 187333 = 35478

Déposez le fichier BiDijkstra.java

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

4 Comptons

Afin de mesurer le gain d’efficacité de la version bidirectionnelle sur la version monodirectionnelle, nous allons compter le nombre d’itérations nécessaires dans chacun des cas.

Modifiez la classe Dijkstra de façon à compter les appels à oneStep().

Complétez les méthodes public int getSteps () dans les classes Dijkstra et BiDijkstra pour qu’elles renvoient chacune le nombre total d’itérations de l’algorithme correspondant.

Testez avec Test4.java. Avec la source et la destination prédéfinies, vous devriez obtenir le résultat suivant. Vous pouvez comparer le nombre d’étapes requises par les deux algorithmes pour d’autres paires (source, destination).

Test 4 : comparaisons du nombre d'étapes
Loading road networks from file data/USA-road-d-NY.gr...    done
Test de la methode getMinPath()...  [OK]

longueur chemin forward    entre 190636 et 187333 = 35478   nombre d'étapes = 1252
longueur chemin backward   entre 190636 et 187333 = 35478   nombre d'étapes = 1204
longueur chemin BiDijkstra entre 190636 et 187333 = 35478   nombre d'étapes = 621

Déposez le fichier BiDijkstra.java

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

Conclusion

C’est fini pour ce module ! Nous espérons que ces TD ont été instructifs.

Preuve de correction de la méthode bidirectionnelle

Soit j le point qui suit i sur a. Alors forward.settled[j] = false et, par conséquent,

forward.dist[j]forward.dist[x]

(sinon j aurait été traité avant x). Puisque a passe par j, nous avons également

forward.dist[j] + backward.dist[j]length(a) < forward.dist[x] + backward.dist[x].

Donc

backward.dist[j] < backward.dist[x].

Comme x est traité par backward, j est aussi traité et donc i est bien dans backward.unsettled. (Il n’est pas possible que backward.settled[i] = true, car x est l’unique sommet traité par les deux recherches.)

Puisque a passe par j, nous avons

forward.dist[i] + backward.dist[i]length(a).

Si cette inégalité était stricte, cela voudrait dire qu’il y a un autre chemin de source (ou de dest) à i plus court que a. Mais alors a ne serait pas parmi les plus courts chemins satisfaisant les hypothèses du lemme.