On peut considérer un labyrinthe comme un ensemble de "cases" dans une grille; une case "vide" indique par exemple un passage, alors qu'une case "pleine" indique un mur :
Un labyrinthe est le plus souvent représenté sous forme d'un tableau de tableaux en mémoire , chaque case étant à l'intersection d'une ligne et d'une colonne de cette grille :
# '0' = case vide / '1' = mur )
laby = [
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1],
[1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1]
]
Le parcours d'un labyrinthe pour en trouver la sortie peut alors être fait en utilisant par exemple la récursivité ( avec les limites que l'on connaît pour les très grands labyrinthes ).
Pour ce projet cependant, on utilisera une approche purement "relationnelle" : le labyrinthe sera modélisé par un graphe, où les cases vides représentent les sommets du graphe, et le fait que deux cases "vides" sont reliés entre elles correspondra à l'existence d'une arête entre ces deux sommets.
Trouver la sortie d'un labyrinthe revient donc à chercher un chemin dans le graphe entre le "sommet d'entrée" du labyrinthe et le "sommet de sortie".
Vous pourrez utiliser la classe Graphe
pour modéliser le labyrinthe en l'implémentant sous forme de liste d'adjacence.
Cependant attention, un sommet ne sera pas désigné par un simple caractère mais sera étiqueté par un tuple contenant les coordonnées ( ligne, colonne) dans la grille de la case correspondant à ce sommet ( cf. image ci-dessus ).
C'est un travail un peu compliqué, mais une fois le graphe créé, il sera très facile de s'en servir pour trouver la sortie du labyrinthe...
Écrire une fonction cree_graphe()
qui prend comme paramètre le tableau de tableaux laby
et crée le graphe modélisant le labyrinthe.
En parcourant le tableau de tableaux passé comme argument, cette fonction devra créer les sommets du graphe mais également les relations qui existent entre deux cases lorsque celles-ci communiquent
( c'est à qu'elles sont adjacentes ).
Ces relations devront a priori être à double sens ( graphe non orienté ).
Pour vérifier que le graphe créé est correct, vous pourrez afficher son dictionnaire de listes d'adjacence, ou utiliser la méthode dessiner
de la classe Graphe
si vous l'aviez
implémentée.
La question : à partir de l'entrée du labyrinthe, quel chemin suivre pour atteindre la sortie ( pas forcément le plus court...) ?
C'est un problème classique pour lequel plusieurs techniques sont possibles pour une personne se trouvant à l'intérieur du labyrinthe.
Mais nous avons la chance d'avoir le plan du labyrinthe ! Laissons alors l'ordinateur déterminer la sortie à notre place...
Écrire une fonction resoudre()
qui prend comme paramètre le graphe modélisant le labyrinthe, le sommet d'entrée et le sommet de sortie, et qui détermine le chemin à suivre pour sortir
du labyrinthe depuis le sommet d'entrée jusqu'au sommet de sortie ( qui doivent bien entendu être dans le graphe...).
La fonction renverra le chemin à suivre sous une forme à déterminer (liste des sommets à suivre par exemple ).
Attention à la manière dont est représenté chaque sommet ( un tuple ) dans le graphe.
Vérifier que la fonction donne le bon résultat !! ( facile à déterminer sur le graphe en exemple...)
La méthode imshow
du module Matplotlib.pyplot permet d'afficher une matrice "2D", avec des couleurs différentes selon la valeur des éléments de la matrice : c'est tout à fait
ce qu'il nous faut pour visualiser le résultat de la recherche de parcours de sortie !
Écrire une fonction affiche
qui :
imshow
pour afficher le labyrinthe et son chemin de sortie.Les fichiers textes ci-dessous contiennent la description de quelques labyrinthes selon le codage suivant : 0 = case vide, 1 = mur, 4 = entrée, 5 = sortie.
Une case correspond à un caractère de chaque ligne.
laby0.txt, laby1.txt, laby2.txt
Écrire une fonction qui extrait les informations de ces fichiers pour en tirer le tableau de tableaux représentant le labyrinthe, puis utiliser les fonctions précédentes pour résoudre le labyrinthe !
De très nombreux algorithmes existent pour générer des tableaux de tableaux représentant un labyrinthe; en voici un qui produit des labyrinthes parfaits, c'est à dire dont chaque case est relié à une autre par un unique chemin.
Il utilise un parcours de graphe en profondeur avec retour en arrière ( backtracking ) :
Cet algorithme suppose de garder la trace des cases déjà explorées, ainsi que celle des débuts de chemins, afin d'y revenir au besoin ("backtracking" ).
Même si l'algorithme ressemble à un parcours de graphe, il n'est pas nécessaire de créer un graphe à proprement parler : c'est le tableau de tableaux qui sera directement modifié.
fonction creer_laby(s: sommet courant):
ajouter s à la liste des cases déjà explorées
si il y a des cases accessibles depuis s et pas encore explorées:
en prendre une au hasard
casser le mur entre elle et s
ajouter s à la liste des débuts de chemin
appeler récursivement la fonction creer_laby à partir de la nouvelle case
sinon:
extraire le dernier début de chemin de la liste des débuts ( si il en reste un )
appeler récursivement la fonction creer_laby à partir de lui
Le plus délicat est de déterminer les cases accessibles depuis une case donnée.
Écrire une fonction creer_laby
qui implémente l'algorithme ci-dessus pour générer un tableau de tableaux représentant un labyrinthe, et l'afficher ensuite.
Le tableau initial ne contenant que des murs est déjà donné, pour un nombre de lignes et de colonnes modifiables.
L'entrée et la sortie du labyrinthe sont placées et peuvent être modifiées.
Le point d'entrée de l'algorithme pourra être n'importe quelle case vide ( pas un mur évidemment ), pas forcément l'entrée ou la sortie.
Dans le problème des seaux "original", il est assez facile de déterminer le chemin à suivre pour aller du sommet (0, 0) au sommet (4, 0), car les deux seaux ont des contenances maximales assez faibles ( 5L et 3L ); qu'en serait-il avec des seaux de contenances beaucoup plus grandes, disons 15L et 7L ? Combien d'étapes seraient nécessaires pour aboutir par exemple à une situation correspondant au sommet (11, 0) ? Et d'abord, existe-t-il même une solution ??
La modélisation "automatique" du graphe correspondant au problème permettrait de s'affranchir de ce travail "à la main", en laissant l'ordinateur déterminer le chemin à suivre si celui-ci existe !
Plutôt que de se concentrer sur un problème particulier, vous allez même écrire un script qui permette la résolution du problème pour n'importe quelles contenances de saut, que vous stockerez par exemple dans deux variables S1 et S2.
Toute la difficulté sera de créer les relations qui doivent exister entre les sommets pour respecter les contraintes du problème, et seulement celles-ci.
Écrire une fonction cree_graphe
qui :
(v1, v2)
représentant toutes les situations possibles pour les volumes v1 et v2 des deux seaux,
avec : 0 ≤ v1 ≤ S1
, et 0 ≤ v2 ≤ S2
.Pour créer ces arcs, on parcourra tous les sommets du graphe, et pour chacun d'entre eux, on se posera la question : avec quel(s) autre(s) sommet(s) doit il y avoir une relation ?
Pour ne rien oublier, voila les "catégories" de relations à modéliser :
C'est clairement la dernière catégorie la plus délicate à modéliser; bien réfléchir à :
En déduire alors entre quels sommets du graphe il faut créer un arc.
La résolution du problème des seaux va donc consister à rechercher le chemin à suivre pour aller du sommet (0, 0)
du graphe à un sommet donné.
Écrire une fonction resoudre
qui prend en paramètre le graphe associé au problème et le sommet d'arrivée, et qui renvoie les étapes à suivre ( sous forme de chaîne de caractères par exemple ),
correspondant au chemin dans le graphe pour résoudre le problème.
On pourra éventuellement dans un premier temps, déterminer si un chemin existe effectivement.
Pour tester la fonction, on rappelle que pour le problème original (S1 = 5 et S2 = 3), le chemin à suivre pour arriver à un seau contenant 4L est :
'(0, 0) -> (5, 0) -> (2, 3) -> (2, 0) -> (0, 2) -> (5, 2) -> (4, 3) -> (4, 0)'
Pour la situation où S1 = 15 et S2 = 7, voila le chemin à suivre pour aller au sommet (11, 0) 😥 :
'(0, 0) -> (15, 0) -> (8, 7) -> (8, 0) -> (1, 7) -> (1, 0) -> (0, 1) -> (15, 1) -> (9, 7) -> (9, 0) -> (2, 7) -> (2, 0) -> (0, 2) -> (15, 2) -> (10, 7) -> (10, 0) -> (3, 7) -> (3, 0) -> (0, 3) -> (15, 3) -> (11, 7) -> (11, 0)'
Rajouter une fonctionnalité qui permette de calculer le nombre d'étapes pour arriver à la solution du problème.
De nombreux petits jeux de réflexion peuvent être résolus grâce à la théorie des graphes et de leur parcours, dans la mesure où il s'agit souvent d'aller d'un état donné du jeu à un autre, en passant par des états intermédiaires : chaque état du jeu peut alors représenter un sommet d'un graphe, et le passage d'un état à un autre les relations entre ces sommets.
La recherche d'un chemin dans ce graphe permet alors de déterminer l'enchaînement des différents états qui permet de résoudre le jeu.
Voila par exemple le célèbre jeu de réflexion Loup-Chèvre-Chou, qui fait partie de la catégorie des "problèmes de passage de rivière" :
Un berger, en compagnie d’un loup, de sa chèvre et d’un chou sur une rive, veut traverser une rivière.
Pour cela, il dispose d’une petite barque qui ne peut contenir que lui et un autre objet ou animal.
De plus, si la chèvre et le chou sont ensemble sur une rive quand le berger s’éloigne, la chèvre mange le chou. Et si le loup et la chèvre sont ensemble quand le berger s’éloigne,
le loup mange la chèvre !
Comment faire pour que tout le monde traverse sans problème ?
Il faut se poser la question de comment représenter la situation : on peut par exemple convenir que chaque état du jeu sera représenté par une chaîne de 3 caractères, telle que :
'0'
indique la rive gauche, un caractère '1'
la rive droite( Si vous préférez utiliser un autre codage de votre choix, help yourself ! )
Au début du problème, l'état du jeu est donc '000'
; il s'agit donc de parvenir à l'état '111'
en suivant les règles du jeu, c'est à dire trouver l’enchaînement des états
'010'
, '110'
, etc.. permis par le jeu.
Chaque état du jeu représentera un sommet d'un graphe. Ces sommets ne sont pas en très grand nombre, ce qui fait que la résolution de ce problème pourrait se faire à la main...mais nous allons laisser Python réfléchir à notre place...
Mais il va falloir mettre à l'épreuve ses capacités d'abstraction !
Dans un premier temps, on créera tous les sommets que peut avoir le graphe modélisant le problème, sans se soucier des relations entre eux.
Écrire le code qui permet de créer un graphe avec la liste de ses sommets, sommets dont l'étiquette sera la chaîne de caractères représentant un état du jeu.
bin()
qui renvoie la représentation binaire d'un nombre entier; le résultat est sous la forme d'une chaîne '0bXX'
, il faudra donc retirer les
caractères '0b'
du début. De plus, pour faire en sorte que chaque code fasse bien 3 caractères, on pourra utiliser la méthode de chaîne zfill(n)
qui permet de rajouter
n caractères '0'
au début de la chaîne à partir de laquelle elle est appelée.
Là aussi, la création des relations peut être tout à fait automatisée.
A l'aide de vos connaissances, et notamment du travail réalisé au chapitre précédent, déterminer dans le graphe établi précédemment un chemin permettant de résoudre le problème ( il existe en réalité deux solutions. )