Le taquin est un jeu inventé à la fin du
XIXe siècle par Sam Loyd et qui a fait l'objet
de nombreuses études depuis. Une présentation de ce jeu est
disponible sur WikiPédia.
Le but de ce projet est de construire une application Java qui
présente à un utilisateur un jeu de taquin (graphique) de nxn
cases et qui lui permet :
Une fonction mélanger proposera aléatoirement une combinaison (mais qui admet une solution). Le nombre de combinaisons possibles est (nxn)!, c'est-à-dire 20 922 789 888 000 pour un taquin de 16 cases. Sur ce nombre, la moitié (environ 1013) admet une solution, et l'autre pas.
La version 0 donnée ci-dessus vous permet d'avoir une idée de
ce que votre application doit faire dans un premier temps. Elle
permet simplement à un utilisateur de jouer ; l'application
contrôle et effectue les déplacements des cases lorsqu'il clique
dessus. La fonction « mélanger » propose une grille aléatoire
mais qui possède une solution. Le bouton « solution » est sans
effet, pour l'instant.....
Remarquez que dans la version présentée ci-dessus la case vide
est en haut à gauche, ce qui oblige à reconstituer en premier la
rangée du bas.
Il est important de noter que votre application doit être
correctement paramétrée pour créer des taquins de tailles
variables. La taille du taquin sera donnée en paramètre programme au
moment de son exécution :
java JeuTaquin 3 oujava JeuTaquin 5
Les exécutions précédentes de
l'application JeuTaquin créent respectivement des
taquins de 9 et 25 cases.
Dans votre application Java, vous distinguerez clairement par
au moins deux classes, la représentation interne du
taquin qui définit un modèle des données du jeu et sa
représentation externe qui permet une visualisation du
taquin à l'écran. Ces représentations doivent
être indépendantes. On pourra, par exemple,
définir deux représentations externes de la représentation
interne, l'une textuelle et l'autre graphique.
Par ailleurs, pour déterminer si deux cases du taquin sont
voisines l'une de l'autre, vous définirez dans une
classe une matrice d'adjacences, bien évidemment
paramétrée sur le nombre de cases du taquin.
La résolution automatique du jeu de taquin peut se faire de
façon directe à l'aide d'un
algorithme ad-hoc, mais il est beaucoup plus
intéressant de mettre en œuvre des algorithmes généraux qui
pourront être utilisés dans des contextes différents pour des
types de problèmes similaires. Ces algorithmes sont des
algorithmes de recherche de plus court chemin dans
un graphe. Pour notre jeu, l'ensemble des
configurations possibles du taquin (rappelons qu'elles sont
nombreuses n2!) est un graphe où
chaque sommet est une configuration particulière du
taquin, et où les arcs relient un sommet
particulier à ses successeurs (entre 2 et 4) correspondants aux
configurations possibles du taquin après un déplacement de la case
vide. Le jeu de taquin consiste à rechercher un chemin, le plus
court si possible, entre deux sommets, en général entre une
configuration quelconque et la configuration initiale du jeu de
taquin.
De nombreux algorithmes existent pour effectuer cette
recherche, mais tous ne peuvent être appliqués tels quels à cause
de la taille du graphe du taquin dès qu'il possède 16
cases. Ainsi,
l'algorithme
de Dijkstra est inutilisable. L'algorithme
appelé A*,
proposé en 1968 par Hart,
Nilsson et Raphael, est une amélioration de l'algorithme de
Dijkstra. C'est cet algorithme que vous utiliserez pour commencer.
Cet algorithme se sert d'une fonction d'évaluation h
(appelée heuristique) qui définit une
estimation inférieure de la distance entre le sommet
courant p et le sommet d'arrivée t. On dit alors
que h est minorante si
∀ p, h(p)≤h*(p),
où h*(p) est la longueur du chemin le plus court
entre p et t. Pour chaque sommet p, on
calcule un coût f = g + h, où g(p)
est la longueur du chemin le plus court connu entre s (le
sommet de départ) et p, et h(p) l'estimation de
la longueur minimale du chemin entre p et le sommet
final t. L'idée de l'algorithme est de passer de sommet
en sommet en privilégiant à chaque fois les sommets qui offrent le
coût f(p) le meilleur, c'est-à-dire en se dirigeant
vers le sommet final en passant par les sommets qui semblent
donner le chemin le plus direct.
Propriété importante : si h est
minorante alors A* est admissible, c'est-à-dire que
l'algorithme donne à chaque fois un chemin minimal
(ou optimal) de s à t, s'il existe.
L'algorithme A*, écrit dans un pseudo-code, est donné ci-dessous :
algorithme A*(s,t) { Hart, Nilsson et Raphaël 1968 }
{recherche du chemin entre le sommet s et le sommet final t }
variables ouverte, fermée : file d'attente
début
trouvé := faux
f(s) := h(s)
ouverte := { s }
répéter
{ retirer le meilleur sommet de ouverte et l'ajouter dans fermée }
courant := leMeilleur(ouverte)
ouverte := ouverte - { courant }
fermée := fermée + { courant }
si courant = t alors trouvé := vrai
sinon
{ poursuivre la recherche du chemin }
pourtout successeur succ de courant faire
f(succ) := g(s, succ) + h(succ)
parent(succ) = courant
si succ ∉ fermée alors
ouverte := ouverte + { succ }
sinon { succ déjà présent dans fermée }
soit aux ce sommet dans fermée
si f(succ) < f(aux) alors
fermée := fermée - { aux }
ouverte := ouverte + { succ }
finsi
finsi
finpour
{ tous les successeurs de courant ont été traités }
finsi
jusqu'à vide(ouverte) ou trouvé
si trouvé alors
reconstituer le chemin à partir de la file fermée
en parcourant les parents depuis le sommet t d'arrivée
sinon
{ la file ouverte est vide }
pas de solution
finsi
fin algo
A* gère deux files d'attente, qui contiennent des sommets du
graphe. La première, appelée ouverte, contient les
sommets examinés au cours du parcours. La seconde,
appelée fermée, contiendra les sommets retenus (ceux dont
le coût f () est le plus bas). C'est à partir de la
file fermée qu'à la fin de l'algorithme le chemin
solution entre le point d'origine s et le point
d'arrivée t sera obtenu, en parcourant les parents depuis
le sommet t.
Vous pourrez tester et comparer les fonctions h suivantes :
Dans l'applet version 1, vous pouvez entrer une configuration de
départ en donnant les valeurs des cases séparées par des
virgules. La case vide possède la valeur 0. Attention, certaines
configurations pourraient ne pas avoir de solution. Dans votre
projet, vous pourrez vérifier a priori l'existence ou
non d'une solution en utilisant la méthode donnée sur la
page WikiPédia.
D'autre part, le slider central permet de faire varier
la vitesse de déplacement des cases lors de l'affichage du
chemin solution.
Selon la dimension du taquin et la configuration de départ, le nombre de sommets traités par l'algorithme A* peut rapidement devenir très important, et conduire à des temps d'exécution prohibitifs pour obtenir une solution. Un certain nombre d'améliorations peuvent être mises en œuvre afin de réduire le nombre de sommets traités et ainsi améliorer les performances de votre application :
L'applet version 2 donnée ci-dessus met en œuvre ces
améliorations. Toutefois, on peut s'apercevoir que pour certaines
configurations de taquins 4x4, la solution trouvée par A* est loin
d'être immédiate. Par exemple, vous pouvez tester la configuration
de départ d'un taquin 4x4 suivante : 0,1,9,7,11,13,5,3,14,12,4,2,8,6,10,15.
Pour obtenir les solutions plus rapidement, il est temps de changer d'algorithme.
Le problème majeur de A* est sa complexité
exponentielle en espace mémoire. Tous les états
qu'il crée sont conservés dans les files fermée
et ouverte ce qui peut, selon les dimensions et l'état
initial du taquin, rapidement saturer la mémoire de l'ordinateur,
même avec les mémoires de grandes capacités actuelles. En 1985,
R. E. Korf propose
l'algorithme IDA* (Iterative-Deepening A*) dont
la complexité en occupation mémoire est linéaire. A tout
moment, il ne conserve en mémoire que les sommets
joignant s au sommet courant p, à l'exclusion de
tous les autres. Contrairement à A* qui traitement d'abord le
sommet de plus petite évaluation, IDA* développe l'état le
plus profond d'abord pourvu que son coût ne dépasse pas un
certain seuil. IDA* procède de façon itérative par
augmentation progressive du seuil. Au départ, le seuil est égal
à h(s). A chaque itération, IDA* effectue une recherche
en profondeur, élaguant chaque branche dont le coût f = g +
h est supérieur au seuil. Si la solution n'est pas trouvée, à
chaque nouvelle itération la valeur du seuil est modifiée, elle
devient égale au minimum des valeurs qui ont dépassé le seuil
précédent. Notez qu' à chaque nouvelle itération le parcours en
profondeur refait tout ou en partie le travail d'exploration du
parcours de l'itération précédente. Cela peut sembler coûteux mais
Korf montre qu'en fait ce ne l'est pas car le plus gros du travail
se fait au niveau le plus profond de la recherche. D'autre part,
si le taquin n'a pas de solution, il faut prévoir de définir
à l'avance un seuil maximal à partir duquel l'algorithme
condidèrera qu'il n'y a pas de solution.
Propriété : Korf a montré que sous les mêmes
conditions que A*, IDA* est complet et admissible.
L'algorithme IDA*, écrit dans un pseudo-code, est donné ci-dessous :
algorithme IDA*(s,t) { Korf, 1985 }
{recherche du chemin entre le sommet s et le sommet final t }
variables chemin : file d'attente
début
seuil := h(s)
résolu := faux
seuil_max = ...
répéter
chemin := { s }
seuil := rechercheEnProfondeur(s, seuil, résolu)
jusqu'à résolu ou seuil > seuil_max
si résolu alors
la file chemin est la solution
sinon
pas de solution
finsi
fin algo
fonction rechercheEnProfondeur(courant, seuil, résolu): entier
début
si h(courant) = 0 alors
résolu := vrai
rendre 0
finsi
nouveau_seuil := +∞
pourtout successeur succ de courant faire
{ ajouter succ dans la file }
chemin := chemin + { succ }
si g(courant,succ) + h(succ) <= seuil alors
{ poursuivre la recherche en profondeur }
b := g(courant, succ) + rechercheEnProfondeur(succ, seuil-g(courant,succ))
sinon { élaguer }
b := g(courant, succ) + h(succ)
finsi
si résolu alors rendre b
{ sinon retirer succ de la file }
chemin := chemin - { succ }
{ et calculer le nouveau seuil pour la prochaine itération }
nouveau_seuil := min(nouveau_seuil, b)
finpour
{ tous les successeurs de courant ont été traités et
la solution n'a pas encore été trouvée =>
renvoyer le seuil pour la prochaine itération }
rendre nouveau_seuil
finfonc
Nombre de visiteurs =