- Cours: M2103 - support ici
- Enseignants: Marin Bougeret, Sébastien Gagné, Victor Poupet, Petru Valicov, Bruno Yun
- Le forum Piazza de ce cours pour poser vos questions
- Email pour une question d'ordre privée concernant le cours.
- Le sujet du TP en format .pdf téléchargeable et imprimable.
Avant de démarrer le TP, vérifiez que vous n'avez pas atteint votre quota d'espace de stockage autorisé :
- placez-vous dans votre
$HOMEet utilisez les commandes suivantes :du -shpour voir combien d'espace vous avez déjà utilisédu -sh *pour voir combien d'espace vous avez déjà utilisé pour chaque fichier (sans fichiers cachés)du -sch .[!.]* *pour voir combien d'espace vous avez déjà utilisé pour chaque fichier, y compris les fichiers cachés
- Supprimez les fichiers inutiles.
- Pour éviter des problèmes durant vos TPs d'informatique, vous devriez toujours garder 300-400 Mo d'espace libre.
- Vous respecterez toutes les consignes indiquées dans le TP4
- Vous essaierez de respecter au maximum les consignes indiquées dans le TP5
Cliquez sur le lien ci-dessous pour faire votre fork privé du TP (attention, pas de fork à la main !):
https://classroom.github.com/a/DWp3d1wf
Date limite de rendu de votre code sur le dépôt GitHub : Vendredi 8 mars à 23h00
L’objectif de ce TP est d’écrire un algorithme qui résout par exploration totale n’importe quel "puzzle". Nous allons illustrer cet algorithme sur un puzzle très simple : un taquin en une dimension. Puis, vous implémenterez dans les Exercices 1, 2 et 3 cet algorithme sur un taquin en deux dimensions. Enfin, vous généraliserez cet algorithme à n’importe quel puzzle dans l'Exercice 4.
Reprenons l’exemple d’un taquin en une dimension à 5 cases. La position initiale (notée 1 2 * 3 4 ) du taquin est
dessinée en haut de la figure ci-dessous :
Le trou se trouve au milieu, avec les palets 1 et 2 à gauche, et 3 et 4 à droite. On considère que la position
gagnante est 1 2 3 4 *.
Nous allons décrire informellement l’algorithme pour résoudre le taquin. Cet algorithme utilise deux variables :
- frontiere : qui va contenir à chaque instant un ensemble de configurations de taquin différentes qu’il reste à examiner
- dejaVues : qui va contenir à chaque instant l’ensemble des configurations déjà examinées.
On initialise frontiere et dejaVues avec la configuration initiale, et on maintiendra l'invariant que frontiere
est un sous-ensemble de dejaVues. À chaque étape, on extrait une configuration de la frontière, on en génère toutes
les configurations "filles" c’est-à-dire les configurations atteignables en effectuant un seul mouvement valide, puis
on ajoute à frontiere et à dejaVues toutes les configurations filles qui n’ont pas été déjà vues. Les ensembles de
configurations a), b) et c), délimités en pointillés, indiquent l’évolution de la frontière lors des 3 premières
étapes (en supposant que lorsque la frontière était égale à b), c'est la configuration 1 * 2 3 4 qui a été extraite).
Remarquez que les configurations barrées ne sont pas ajoutées à la frontiere (ni à dejaVues) puisqu’elles sont déjà
présentes dans dejaVues au moment où l'on essaye de les ajouter. L’algorithme se termine lorsqu’il atteint une
configuration gagnante, ou lorsque la frontière devient vide. Ainsi on obtient une structure arborescente (ou arbre
d'exploration) représentant l'ensemble de mouvements valides obtenus à partir de la racine (configuration initiale).
Remarque : Certaines configurations du taquin ne sont pas résolubles. En une dimension il est facile de voir que les
cases ne peuvent pas se croiser (donc par exemple la configuration 2 1 * 3 4 ne peut pas être résolue) et en deux
dimensions on peut montrer que la moitié des configurations initiales possibles n'admettent pas de solution (cf.
page Wikipédia pour une caractérisation).
L'algorithme expliqué ci-dessus, permet de résoudre le taquin, à savoir obtenir la configuration finale gagnante si elle
existe. Dans ce qui suit, on vous demandera également de stocker la trace de la solution, qui indique les configurations
obtenues à chaque étape intermédiaire pour arriver à la solution finale. Avoir la trace est intéressant pour
un utilisateur, afin de voir la stratégie à adopter pour résoudre le puzzle à partir de la configuration initiale. C'est aussi pratique pour vérifier si votre programme fonctionne correctement... La trace de la solution va correspondre à une liste chaînée de configurations construite de la façon suivante : lorsqu’une configuration c2 est générée à partir d’une configuration c1, on mémorisera que
le "père" de c2 est c1. Un maillon de cette liste chaînée est donc un couple (configuration taquin, couple parent).
La classe Taquin vous est donnée dans le package fr.umontpellier.iut. Vous devez la compléter comme suit :
-
Ajoutez un attribut de type
int[][]qui représentera le plateau de jeu et implémentez un constructeur sans paramètres qui place le taquin dans une configuration initiale de votre choix. On representera le trou du plateau par le chiffre0. -
Redéfinissez la méthode
toString()(de la classeObject) afin d'afficher le contenu du plateau deTaquin. -
Complétez la méthode
public boolean estGagnant()afin qu'elle retourne vrai si le plateau est dans une configuration gagnante et faux sinon. Pour un taquin en deux dimensions, la position gagnante est la configuration :
+-----+
|1 2 3|
|4 5 6|
|7 8 0|
+-----+
-
Complétez la méthode
public ArrayList<Taquin> genererFils()qui retourne la liste des objetsTaquinque l’on peut obtenir en faisant un mouvement valide. -
Redéfinissez la méthode
equals(Object o)de la classeObjectafin qu'elle permette de comparer leTaquincourant avec un autre passé en paramètre.Astuce : Nous vous conseillons d'utiliser votre IDE pour redéfinir
equals(Object o)et de prendre le temps de comprendre le code qu'il vous générera. Vous ajusterez cette redéfinition, en fonction de la logique du code de votre classeTaquin. Prêtez également attention à la redéfinition de la méthodepublic int hashCode()deObjectqui va être faite. Discutez-en également avec votre enseignant (voir également le cours). -
Vérifiez votre programme en instanciant dans la classe principale (
App) des taquins avec des configurations initiales (parfois différentes, parfois égales) et comparez-les pour s'assurer que la méthodeequals(Object o)fonctionne correctement. Pour chacun des taquins instanciés, vous afficherez la liste des fils retournée pargenererFils().
Rappelez-vous que nous aurons besoin de "couples chaînés" pour pouvoir retrouver la suite des coups effectués lorsque
l’algorithme trouve une position gagnante. C'est pour cela que la classe Couplevous est donnée. Complétez cette classe
de la façon suivante :
-
Ajoutez un attribut
taquinde typeTaquinet un attributpredecesseurde typeCouple. Ajoutez un constructeur pour initialiser ces deux attributs. -
Complétez la méthode
public ArrayList<Taquin> getListeDeMouvements()ayant les spécifications suivantes :- hypothèse : le couple courant (
this) représente une solution ayant été atteinte depuis la racine de l’arbre d’exploration (on a donc un chaînage du typenull← couple_racine ← couple_1 ← ... ← couple_k ← couple courant) - effet : retourne une
ArrayList<Taquin>de la forme[couple_racine.t, couple_1.t,..,couple_k.t, couple_courant.t], qui correspond donc à la description de la solution trouvée
- hypothèse : le couple courant (
-
Complétez la méthode
public void mettreAJour(ArrayList<Couple> frontiere, ArrayList<Taquin> tab, ArrayList<Taquin> dejaVus)pour qu'elle respecte la spécification ci-dessous. Avant de lire cette spécification, considérons l'exemple dans lequelcoupleGauchereprésente le couple dont le taquin est celui de gauche dans la frontière b) de l'exemple ci-dessus (et son prédécesseur pointe sur la racine)frontiereest l'ensemble deCoupledont les taquins sont ceux de b)tabest l'ensemble des 2 taquins fils du taquin contenu danscoupleGauche(* 1 2 3 4et1 2 * 3 4)dejaVusest l'ensemble des 3 taquins de a) U b).
Dans cet exemple,
coupleGauche.mettreAJour(frontiere,tab,dejaVus)doit ajouter le taquint = * 1 2 3 4àdejaVusainsi que le couple(t,coupleGauche)àfrontiere, et ne rien faire pour le taquin1 2 * 3 4puisqu'il est déjà dansdejaVus.La spécification est donc la suivante :
mettreAJourajoute àfrontiere(et àdejaVus) tous les couples(t,this)avectappartenant àtab, et tels quetn’est pas dansdejaVus.Remarque : Ici nous vous recommandons d'utiliser entre autres la méthode
boolean contains(o)définie dansArrayListqui renvoie vrai sioappartient à l'objetArrayList. Expliquez pourquoi ce test d'appartenance fonctionnera correctement si on l'invoque sur un objetArrayList<Taquin>. -
Testez en écrivant un test unitaire qui crée 3 taquins
t1,t2,t3, puis unCouplecontenant le chaînagenull←t1←t2←t3(t1joue donc le rôle de la racine), et qui vérifie quegetListeDeMouvements()appelé sur ce couple retourne le bon résultat.Astuce : On rappelle que pour créer une classe de tests unitaires, placez-vous dans la classe que vous souhaitez tester (dans notre cas c'est
Couple) et :- appuyez sur Alt+Insert (ou bien faites un clic droit sur le nom de la classe → Generate)
- Choisissez Test...
- dans l'onglet Testing library vous choisirez l'option JUnit 5
- donnez un nom approprié à votre classe de tests unitaires (par ex.
CoupleTest) et cliquez sur Ok.
Astuce : Pour créer ce
Couple, on dispose seulement du constructeurCouple(Taquin taquin, Couple predecesseur), qu'il faudra utiliser plusieurs fois. Commencez par créer le couplec = Couple(t1, null), puis servez-vous decpour construire les autres couples.
La classe Contexte va encapsuler l'algorithme général de résolution du jeu. Complétez cette classe comme suit :
-
Ajoutez un attribut
Taquin taquin(qui servira à stocker leTaquininitial donné à l'objetContexte), et un attributsolutionde typeArrayList<Taquin>. L'attributsolutionservira à stocker la trace des mouvements valides que l'algorithme a effectués depuis la position donnée par l'utilisateur, afin d'obtenir une position gagnante. -
Ajoutez un constructeur
Contexte (Taquin taquin). -
Complétez la méthode
public void resoudre()afin qu'elle affecte à l'attributsolutionuneArrayList<Taquin>vide sitaquinn’est pas faisable, ou la liste des positions successives qui mènent à un état gagnant sinon. -
Dans votre méthode
resoudre(), il y a plusieurs façons de gérer votre frontière :- comme une pile : le taquin extrait à chaque nouvelle étape est le dernier taquin a avoir été ajouté. Dans ce cas l'exploration de l'arbre se fera en profondeur (c'est-à-dire que l'on termine complètement une branche avant de passer à la suivante).
- comme une file : le taquin extrait à chaque nouvelle étape est le premier a avoir été ajouté. Dans ce cas l'exploration de l'arbre se fera en largeur (tous les taquins à distance 1 de la racine, puis tous les taquins à distance 2, etc.).
Regardez dans votre code de la question précédente si votre frontière est gérée en pile ou en file, et réfléchissez à la politique de gestion (pile vs file) que vous préférez.
-
Redéfinissez la méthode
toString()afin d'afficher la solution. -
Testez d'abord avec des taquins que l'on peut résoudre. Pour cela, créez un taquin à distance 1 de la position gagnante (c'est-à-dire nécessitant un mouvement pour le résoudre), puis à distance 2, puis à distance k > 2. Ensuite, testez avec un taquin quelconque. Si votre algorithme s’exécute pendant plusieurs minutes, comment essayer de savoir s'il est dans une boucle infinie ou si "quelque chose" progresse ? Quelle(s) donnée(s) pourriez vous afficher (même si cela ralentit énormément l'algorithme) pour répondre à cette question ?
Maintenant nous allons généraliser cette stratégie à la résolution d'autres jeux de type "puzzle". Afin de garder un historique du programme écrit précédemment, nous allons travailler dans un package différent.
-
Créez un nouveau package
fr.umontpellier.iut.exo4et copiez/collez les 3 classes écrites précédemment. Pour faire cela correctement, la manière la plus simple est de sélectionner en même temps les 3 classes dans l'IDE → Copier → Coller dans le package. Quelque soit la manière dont vous allez procéder, l'IDE vous signalera des duplications de code (logique, car c'est ce qui vous avez fait), mais dans ce cas (et pour ce genre de duplications demandées) vous allez ignorer ces avertissements car c'est un moyen simple de garder une copie de ce que vous avez fait dans les exercices précédents. -
Observez que les fonctions "essentielles" de la classe
Taquinsont suffisamment générales pour être appliquées sur d'autres jeux de même nature. Ajoutez donc au packagefr.umontpellier.iut.exo4une interfaceJeuPuzzleavec les méthodes en question. -
Faites en sorte que Taquin soit une implémentation de l'interface
JeuPuzzleet modifiez votre programme pour que cela ait du sens et fonctionne.
Nous allons maintenant utiliser cette interface pour implémenter un autre jeu : les tours de Hanoï. Dans ce jeu on considère 3 poteaux (dénommés "1" (à gauche), "2" (au milieu), et "3" (à droite)), ainsi que N disques de diamètres deux à deux distincts. Les disques sont troués en leur centre, de telle sorte que l'on puisse les enfiler sur les poteaux. Dans la situation initiale, les N disques sont sur le poteau gauche, et rangés "en pyramide" : c'est-à-dire de telle sorte que les plus petits disques sont au-dessus. Le but du jeu est de déplacer cette pyramide sur le poteau de droite, en sachant qu'un coup légal consiste à
- choisir un poteau de départ, et prendre le disque du dessus
- choisir un poteau d'arrivée, et déposer le disque au sommet
- s'assurer que sur chaque poteau les disques restent rangés en pyramide (autrement dit, un disque ne peut être placé que sur un disque de plus grand diamètre).
Par exemple, pour N=3 la succession de coups 1 → 2 (signifiant prendre le disque au sommet du poteau 1 et le placer au sommet du poteau 2) 1 → 3, 2 → 3 est légale, alors que la succession de coups 1 → 2, 1 → 2 ne l'est pas.
-
Écrivez une classe
Hanoiqui modélise ce jeu et qui implémente l'interfaceJeuPuzzle. Pour modéliser l'état du jeu, on suggère d'utiliser troisArrayList<Integer>contenant chacune les numéros des disques présents sur le poteau correspondant. Vous pouvez également ajouter un attributprivate int taillepour indiquer le nombre initial de disques. Une configuration du jeu correspondrait aux 3 poteaux contenant en tout les N disques. Chaque mouvement de disque autorisé d'un poteau vers un autre est une nouvelle configuration (nouveau fils donc). -
Modifiez la classe principale pour maintenant tester la résolution de Hanoï (commencez par 3 disques sur le poteau gauche). On constate (avec joie !) qu'il n'y a pas à modifier l'algorithme de résolution puisqu'il fonctionne de façon "transparente" pour tout
JeuPuzzle. -
Dessinez le diagramme de classes de cet exercice.
Remarque : Cette façon de programmer, en proposant une interface d'algorithme générale qui sera ensuite implémentée
différemment, et dont les implémentations pourront être interchangées "à la volée" par l'utilisateur dans la classe cliente
(ici App), fait référence au modèle de conception communément appelé
Stratégie.

