DM 1 : VISITE TOURISTIQUE DE PARIS EN MÉTRO Dans ce devoir

Transcription

DM 1 : VISITE TOURISTIQUE DE PARIS EN MÉTRO Dans ce devoir
DM 1 : VISITE TOURISTIQUE DE PARIS EN MÉTRO
PHILIPPE CHASSIGNET ET DOMINIQUE ROSSIN
Dans ce devoir à la maison, nous continuons à travailler sur le plan du métro parisien.
Nous voulons trouver un plus court chemin entre deux stations données. Ce plus court
chemin peut être vu de différentes manières:
• Plus petit nombre de stations intermédiaires
• Plus petit nombre de changements
• Prise en compte des temps de trajet et des temps de correspondance
La partie 1 bien traitée suffira pour obtenir la note A.
1. Plus petit nombre de stations
On part du TD 1 avec les mêmes classes et structures de données. On cherche le plus
court chemin entre deux stations en minimisant le nombre de stations intermédiaires. À
l’aide d’un parcours en largeur du graphe représentant le métro, on écrit la classe Situ1
qui, à partir de deux stations de départ et d’arrivée, calcule un plus court chemin:
java Situ1 lignes.data depart arrivee
où lignes.data est le nom du fichier contenant le plan du métro, depart le numéro de
la station de départ et arrivee le numéro de la stations d’arrivée. La sortie de votre
programme devra être de la forme:
De la station depart a la station arrivée
1. Prendre la ligne nomLigne direction terminusLigne
2. Changer à la station nomStation
3. Prendre la ligne nomLigne direction terminusLigne
4. ...
2. Prise en compte de la distance euclidienne
À présent, nous tenons compte du temps de parcours d’une station à une autre. Une
première approximation du temps de parcours est proportionnelle à la distance parcourue
entre ces stations.
À cette fin, on rajoute dans la classe Station située dans le fichier MetroUtil.java la
fonction suivante:
static final int COUT_ARRET = 10;
static final double COEFF_DISTANCE = 1.5;
static int tempsParcours(int ori, int dst) {
int xOri = Station.stations[ori].x, yOri = Station.stations[ori].y;
int xDst = Station.stations[dst].x, yDst = Station.stations[dst].y;
int dX = xDst - xOri;
int dY = yDst - yOri;
double dist = Math.sqrt( dX*dX + dY*dY );
return COUT_ARRET+(int)(COEFF_DISTANCE*dist);
}
1
2
PHILIPPE CHASSIGNET ET DOMINIQUE ROSSIN
Ainsi, un appel à Station.tempsParcours(3,5) renvoie le temps mis par le métro pour aller
de la station 3 à la station 5. Attention, cette fonction ne vérifie pas si les stations sont
reliées entre elles. Elle renvoie un temps qui est la somme du temps de parcours effectif du
métro (dépendant de la distance euclidienne entre les stations) et d’un temps d’arrêt à la
station.
Ainsi, le plus court chemin entre deux stations ne sera plus cette fois un chemin qui minimise le nombre de stations, mais qui minimise le temps de parcours. Les correspondances
ne sont toujours pas prises en compte. (temps de correpondance nul)
Le calcul du plus court chemin sera fait par l’algorithme de Dijkstra simplifié, c’est-à-dire
que l’on utilisera un tableau au lieu de faire une file de priorité (cf. cours 3). On écrit la
classe Situ2 qui, à partir de deux stations de départ et d’arrivée, calcule un plus court
chemin:
java Situ2 lignes.data depart arrivee
où lignes.data est le nom du fichier contenant le plan du métro, depart le numéro de
la station de départ et arrivee le numéro de la stations d’arrivée. La sortie de votre
programme devra être comme dans la section précédente.
3. Première extension – Mise en œuvre d’une file de priorité
Les deux extensions proposées sont indépendantes et peuvent être traitées dans un ordre
quelconque. La première extension consiste à mettre en œuvre une file de priorité pour
l’algorithme de Dijkstra.
Dans notre cas, un élément de la file de priorité est une paire qui associe un numéro de
station à sa priorité, c’est-à-dire le temps de parcours pour cette station. L’élément en tête
de la file, le plus prioritaire donc, est celui ayant le plus petit temps de parcours à partir
de la station de départ.
Une file de priorité supporte deux opérations:
• ajouter un élément dans la file, il sera traité en fonction de sa priorité,
• sortir l’élément en tête de file (une fonction qui retourne cet élément),
La structure de tas (heap en anglais) permet de réaliser chacune de ces deux opérations
en temps O(log n), n étant le nombre d’éléments dans la file, majoré ici par le nombre de
sommets du graphe.
Rappelons qu’un tas de n éléments est un arbre binaire complet rangé dans un tableau
t dont on utilise alors les indices de 0 à n − 1. La racine est t[0]. Pour un élément t[i],
les fils gauche et droit, s’ils existent, sont respectivement t[2*i+1] et t[2*i+2]. Pour un
élément t[i] (i > 0), le père est t[(i-1)/2].
De plus, un tas maintient une relation d’ordre partiel entre père et fils. Dans notre cas,
le père aura toujours un temps de parcours inférieur ou égal à ceux de ses fils. L’élément
le plus prioritaire de tous se trouve donc à la racine.
Pour ajouter un élément dans un tas, de capacité actuelle n, on commence par le placer
en t[n], puis on procède à des échanges en le remontant vers la racine, jusqu’à satisfaire
de nouveau la relation d’ordre.
Pour sortir l’élément de tête d’un tas, de capacité actuelle n, on récupère t[0], on place
t[n-1] en t[0], puis on procède à des échanges avec le plus prioritaire des deux fils, en
redescendant dans l’arbre jusqu’à satisfaire de nouveau la relation d’ordre.
Remarquons que ces deux opérations reviennent à se déplacer seulement le long d’une
branche, d’øù leur coût en O(log n).
DM 1 : VISITE TOURISTIQUE DE PARIS EN MÉTRO
3
La difficulté de l’algorithme de Dijkstra vient de ce qu’il nécessite une troisième opération
sur la file de priorité :
• modifier la priorité d’un élément donné.
Chaque élément mémorise sa position dans le tas. Un élément du tas est un triplet associant
un numéro de station, le temps de parcours pour l’instant et son indice dans le tableau du
tas. Une modification revient à améliorer la priorité (ie. réduire le temps de parcours), on
procéde alors à des échanges en remontant vers la racine, comme pour un ajout, sauf que
l’on part de la position donnée par l’indice mémorisé. Il faut bien sûr que les opérations
d’échange entre père et fils mettent à jour les indices mémorisés pour assurer la cohérence
de la structure.
Pour ne pas avoir à se préocupper si l’élément est présent ou non, on commencera par
remplir la file avec tous les éléments, initialisés à une priorité ad-hoc. Ensuite, l’algorithme
de Dijkstra nous assure qu’on ne cherchera à modifier que des éléments encore présents
dans la file.
On écrit la classe Situ3 qui, à partir de deux stations de départ et d’arrivée, calcule un
plus court chemin:
java Situ2 lignes.data depart arrivee
avec le même format de sortie que dans les deux premières sections.
4. Deuxième extension – Prise en compte des temps de correspondance
Par exemple, le plus court chemin (le plus rapide) de Bastille à République consiste
à prendre la ligne 8 et le plus court chemin de Bastille à Jacques Bonsergent consiste à
prendre la ligne 5. Pourtant, le plus court chemin de Bastille à Jacques Bonsergent consiste
à prendre uniquement ligne 5 pour éviter le changement de ligne.
De même, pour aller de La Bastille à La Madeleine, on prendra la ligne 8 et pour aller
de La Madeleine à Saint-Lazare, on prendra la ligne 12. Le plus rapide de La Bastille à
Saint-Lazare est plutôt de prendre la ligne 1 jusqu’à Concorde puis la ligne 12, passant par
La Madeleine.
Ces deux exemples nous montrent qu’il faut repenser la structure du graphe. On éclate
chaque sommet en plusieurs sommets pour distinguer :
• un sommet cœur de station, supposé être le point de concentration de tous les couloirs,
• un sommet pour chaque quai, ie. chaque ligne et chaque sens desservant la station.
Quand le graphe initial avait un arc de la station A vers la station B, on aura maintenant
3 arcs :
• du cœur de A jusqu’au bon quai, son temps de parcours sera très long, par exemple
300 (secondes), pour prendre en compte aussi le temps d’attente de la prochaine rame,
• du quai de la station A pour la ligne considérée vers le quai pour cette même ligne à
la station B, son temps de parcours sera celui utilisé auparavant,
• du quai de la station B, vers le cœur de B, avec un temps de parcours de l’ordre de
60 (secondes), par exemple.
Les itinéraires suivront naturellement les lignes en passant d’un sommet quai au suivant.
On ne passe par un cœur de station qu’au départ, à l’arrivée et lors d’un changement de
ligne.
Une petite difficulté technique est d’assurer l’indexation de tous ces sommets. Là où
un numéro de station suffisait, il faut maintenant un triplet numéro de station, numéro de
ligne et code de direction. Ces deux derniers doivent être extraits de la chaı̂ne de caractère
qui désigne la ligne. On utilisera pour cela les deux fonctions suivantes :
4
PHILIPPE CHASSIGNET ET DOMINIQUE ROSSIN
static int numeroLigne(String ligne)
static int codeDirection(String ligne)
*** PREVOIR UN LIEN VERS LE CODE FOURNI ***