Cours BUT Info

TP 2 — Le labyrinthe

Sortir d'un labyrinthe en explorant ses cases : parcours en profondeur avec une pile, puis en largeur avec une file — corrigés testés, pièges et observations.

Ce TP est le passage à la pratique du chapitre 2 : on explore un labyrinthe en suivant un algorithme, d'abord avec une pile (parcours en profondeur), puis avec une file (parcours en largeur). Le fichier Labyrinthe.java est fourni : il contient le labyrinthe, les méthodes pour le tester et un affichage graphique. Notre travail : écrire la classe Cellule et la classe Parcours dans le même paquetage tp2, en marquant les cases au fur et à mesure.

Six questions : la cellule (Q1), le parcours avec pile (Q2), la sortie bouchée (Q3), le parcours avec file (Q4), puis les versions « maison » avec la Pile et la File du TD 2 (Q5, Q6).

Le labyrinthe fourni

Le labyrinthe est un tableau de caractères de 41 × 41 : un # est un mur, un espace est un passage. L'entrée est en haut à gauche, aux coordonnées (0, 1) — x = colonne, y = ligne — et la sortie en bas à droite, à la dernière colonne et l'avant-dernière ligne. La classe fournie sait tout faire sauf se parcourir : elle expose de quoi tester un mur, une sortie, poser une marque (rose pour une case explorée, rouge pour le chemin final) et rafraîchir la fenêtre.

Un mini-labyrinthe : on entre en haut à gauche, on sort à droite. Les cases explorées restent marquées ; le chemin final est la pile au moment où la sortie est atteinte. Le cul-de-sac (pastille grise) a été dépilé en cours de route.

Q1 — la classe Cellule

Le parcours manipule des couples (x, y) : on commence donc par une petite classe Cellule, avec les deux attributs et un constructeur, plus un toArray() qui rend service pour les méthodes en version int[] de Labyrinthe.

Q2 — le parcours avec une pile

L'algorithme demandé, en français :

  1. Empiler la cellule d'entrée et la marquer.
  2. Tant que la pile n'est pas vide :
    • soit c la cellule au sommet de la pile ;
    • si c est la sortie → afficher « Sortie trouvée » et s'arrêter ;
    • sinon, s'il existe une voisine de c qui n'est ni un mur ni une case marquée : la marquer et l'empiler (une seule par itération, même si plusieurs sont libres) ;
    • sinon, dépiler : c'est un cul-de-sac.
  3. Si la pile se vide avant d'avoir trouvé la sortie : aucun chemin n'existe.
La boucle du parcours, répétée tant que la pile n'est pas vide. Si la pile se vide avant la sortie : il n'y a pas de chemin.

À la fin, la pile contient une chaîne de cellules voisines de l'entrée jusqu'à la sortie : c'est le chemin, qu'on marque en rouge. Petit détail qui change tout : on marque une case au moment où on l'empile (pas au moment où on la dépile) — c'est ce qui empêche de revenir cent fois sur la même case.

Ce que vous devez voir à l'écran

La fenêtre se remplit de rose (les cases explorées) au fil du parcours, et dès la sortie atteinte, le chemin apparaît en rouge. Sur le labyrinthe fourni, avec l'ordre de test des voisines droite → bas → gauche → haut : 231 cases explorées et un chemin de 139 cases. Supprimez le ralentir() pour aller au bout instantanément.

Q3 — et si la sortie était bouchée ?

Le sujet demande de modifier le labyrinthe pour voir le comportement quand la sortie est inatteignable : on transforme la case de sortie en mur. C'est la dernière ligne du tableau murs (indice 39) qui contient la sortie — son dernier caractère doit passer de l'espace à #.

Labyrinthe.java — la ligne 39 avant / après
// avant :
"#     # #  # #             #      # # #  ",
// après (la sortie est murée) :
"#     # #  # #             #      # # # #",

Relancez : le rose se répand, se replie sur lui-même (les cul-de-sac se dépilent), puis la pile se vide et le programme affiche :

Il n'y a pas de chemin reliant l'entrée à la sortie

Toutes les cases atteignables depuis l'entrée sont alors marquées en rose : 836 cases sur le labyrinthe fourni. Et c'est ce scénario qui fait planter la version naïve du test des voisines quand la boucle revient jusqu'à l'entrée (voir le piège des bords plus haut) — avec le test des bornes, pas de problème.

Q4 — la même exploration avec une file

On remplace la pile par une file : on retire la tête (poll) et on ajoute toutes les voisines explorables en queue (offer). Le parcours progresse en largeur : d'abord la case d'entrée, puis ses voisines, puis les voisines de ses voisines… Le sujet propose de le faire dans Parcours ; on préfère créer ParcoursFile.java pour garder les deux versions (bon réflexe : un fichier = une responsabilité).

Comme la file ne suit pas un chemin unique, il faut retenir d'où chaque case a été découverte pour reconstruire le chemin : un HashMap<Cellule, Cellule> qui associe chaque case à son « père » — d'où le equals/hashCode de la Q1.

L'observation la plus intéressante du TP

Même labyrinthe, conteneur différent, comportements opposés (mesurés en test) :

pile — Q2file — Q4
Cases explorées231836 (toutes celles atteignables)
Chemin trouvé139 cases93 cases — le plus court

C'est logique : la file découvre les cases par distance croissante depuis l'entrée, donc la première fois qu'elle touche la sortie, c'est forcément par le plus court chemin. La pile, elle, plonge au bout de la première branche venue : elle trouve un chemin, pas le meilleur. (Le sujet demande seulement d'observer ce changement ; on est allés un cran plus loin en reconstruisant le chemin rouge via les pères.)

Q5 — avec la Pile du TD

Remplaçons java.util.Stack par notre classe Pile<E> du TD 2 (elle est dans le même paquetage, rien à importer). Le sujet demande que « le comportement reste exactement le même » : c'est le cas, car notre pile expose les mêmes noms de méthodes. Deux différences quand même :

// 1) la création (et supprimer l'import java.util.Stack)
Pile<Cellule> p = new Pile<>(l.n() * l.n()); // au lieu de : new Stack<>()

// 2) le marquage du chemin : java.util.Stack se parcourt au for-each,
//    notre Pile maison se draine simplement (l'ordre des marques n'importe pas)
while (!p.isEmpty()) {
    Cellule c = p.pop();
    l.poserMarqueChemin(c.x, c.y);
}

Pourquoi n() * n() comme capacité ? Une case n'est empilée qu'une fois au plus (on la marque au moment de l'empiler) : 41 × 41 = 1681 suffit toujours. En test : mêmes 231 cases explorées et même chemin de 139 cases qu'avec java.util.Stack — comportement identique, comme prévu.

Q6 — avec la File du TD

Même figure pour la question 4 : on remplace ArrayDeque par notre File<E> — les noms offer/poll/isEmpty sont les mêmes que ceux de Queue, et peek aussi existe chez nous.

// dans ParcoursFile.explorer (et supprimer l'import java.util.ArrayDeque)
File<Cellule> file = new File<>(l.n() * l.n()); // au lieu de : new ArrayDeque<>()

En test : mêmes 836 cases explorées, même chemin de 93 cases. Nos deux conteneurs du TD sont donc de vrais remplaçants d'usage général — c'était tout l'intérêt de les écrire nous-mêmes.

Ce qu'il faut savoir refaire

  • écrire une boucle d'exploration : regarder le sommet, tester la sortie, chercher une voisine explorable, l'empiler, sinon dépiler ;
  • marquer au moment d'empiler — jamais au dépilement ;
  • tester les bornes avant estMur/estMarque (explorable) ;
  • le chemin final est la pile elle-même (elle forme une chaîne de voisines) ;
  • pour la version file : ajouter toutes les voisines, et retenir les pères pour reconstruire le chemin — c'est le plus court.

Le fichier fourni


tp2-labyrinthe.pdf

TP Piles et Files — Le labyrinthe · Structures de données · 3 pages

Consultation seule — téléchargement désactivé

Chargement du document…

Dernière mise à jour le

Sur cette page