« Génération et résolution de labyrinthes II » : différence entre les versions
Aucun résumé des modifications |
|||
Ligne 1 : | Ligne 1 : | ||
__FORCETOC__[[Fichier:labyrinthe_photo.png|vignette|300px| |
__FORCETOC__[[Fichier:labyrinthe_photo.png|vignette|300px|Exemple de labyrinthe généré et résolu. En rouge foncé, l'entrée. En vert, la sortie. En bleu, le chemin entre le départ et l'arrivée. En rouge clair, les cellules visitées lors de la résolution.|alt="Labyrinthe"]] |
||
On veut générer des labyrinthes aussi grands et complexes que possible, avec des murs dans une grille carré. Comment faire pour qu'il y ait toujours un chemin de l'entrée à la sortie ? Comment faire pour qu'il n'y ait qu'un chemin ? Et comment trouver la sortie quand on est perdu dans le labyrinthe ? |
On veut générer des labyrinthes aussi grands et complexes que possible, avec des murs dans une grille carré. Comment faire pour qu'il y ait toujours un chemin de l'entrée à la sortie ? Comment faire pour qu'il n'y ait qu'un chemin ? Et comment trouver la sortie quand on est perdu dans le labyrinthe ? |
||
Ligne 66 : | Ligne 66 : | ||
En plaçant un point sur chaque cellule de notre labyrinthe, et en reliant deux cellules séparées par une porte, on peut dessiner un arbre à partir de n'importe quel labyrinthe. |
En plaçant un point sur chaque cellule de notre labyrinthe, et en reliant deux cellules séparées par une porte, on peut dessiner un arbre à partir de n'importe quel labyrinthe. |
||
Sur notre graphe, un arête est donc un passage dans notre labyrinthe. |
|||
[[Fichier:labyrinthe_avec_arbre.png|vignette|centre|300px|Exemple de labyrinthe avec son arbre. Pour plus de simplicité, on attribue à chaque nœud une position.|alt=Labyrinthe avec arbre]] |
[[Fichier:labyrinthe_avec_arbre.png|vignette|centre|300px|Exemple de labyrinthe avec son arbre. Pour plus de simplicité, on attribue à chaque nœud une position.|alt=Labyrinthe avec arbre]] |
||
Ligne 80 : | Ligne 82 : | ||
Chaque algorithme est différent et produit des labyrinthes visuellement différents. |
Chaque algorithme est différent et produit des labyrinthes visuellement différents. |
||
⚫ | |||
⚫ | |||
[[Fichier: |
[[Fichier:recursive_backtracker.gif|vignette|left|500px|Illustration du parcours en profondeur|alt=Parcours en profondeur]] |
||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
Ligne 97 : | Ligne 102 : | ||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
Ligne 113 : | Ligne 130 : | ||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | * find(a) -> <math>\alpha</math> : tant que l'élément n'est pas son propre représentant, on applique find au représentant de l'élément.<br> On commence donc par find(a), puis find(A) (si 'A' est le représentant de 'a'), etc... La méthode de la compression de chemin permet d'éviter trop de récursivité en écrivant : représentant de 'a' = <math>\alpha</math> |
||
⚫ | |||
⚫ | |||
⚫ | |||
=== Comparaison === |
|||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
Ligne 146 : | Ligne 171 : | ||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
Ligne 174 : | Ligne 187 : | ||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | * find(a) -> <math>\alpha</math> : tant que l'élément n'est pas son propre représentant, on applique find au représentant de l'élément.<br> On commence donc par find(a), puis find(A) (si |
||
⚫ | |||
⚫ | |||
⚫ | |||
== Résolution == |
== Résolution == |
||
Ligne 261 : | Ligne 263 : | ||
On remarquera que des deux algorithmes, celui du mur droit explorera toutes les cellules situées en dessous du chemin optimal. Il sera donc plus efficace lorsque le chemin de solution est vers le bas de la grille. |
On remarquera que des deux algorithmes, celui du mur droit explorera toutes les cellules situées en dessous du chemin optimal. Il sera donc plus efficace lorsque le chemin de solution est vers le bas de la grille. |
||
== Bilan personnel == |
|||
Ce projet m'a permis de découvrir plusieurs structures de données qui me seront utiles à l'avenir. |
|||
J'ai aussi pu me familiariser avec tkinter, avec la programmation orientée objets en Python (notamment héritage et méthodes statiques) et avec le multithreading (j'avais codé mon projet en programmation parallèle avant de le refaire entièrement un un seul thread une semaine avant l'oral). |
|||
J'ai aussi pu comprendre la complexité mathématique derrière un labyrinthe. |
|||
== Code source == |
== Code source == |
Version du 22 mai 2019 à 17:30
On veut générer des labyrinthes aussi grands et complexes que possible, avec des murs dans une grille carré. Comment faire pour qu'il y ait toujours un chemin de l'entrée à la sortie ? Comment faire pour qu'il n'y ait qu'un chemin ? Et comment trouver la sortie quand on est perdu dans le labyrinthe ?
Représentation
Propriétés
Un labyrinthe est une grille de cellules reliées, ou non, entre elles.
Deux cellules sont reliées entre elles par une porte, ou séparées par un mur.
Tout labyrinthe a une entrée et une sortie, et quelle que soit l'entrée ou la sortie, le chemin entre ces deux cellules est unique.
Modélisation en graphe
En théorie des graphes, un graphe est un ensemble de points (ou nœuds ou sommets) reliés ensemble par des lignes (ou liens ou arêtes).
Un graphe peut être connexe (en un seul morceau) ou non connexe (en plusieurs morceaux).
Un graphe peut aussi être cyclique si les points sont reliés en formant une boucle ou acyclique.
Enfin, un graphe peut être orienté (si les arêtes sont à sens unique) ou non orienté.
Un arbre est un graphe connexe, acyclique et non orienté.
En plaçant un point sur chaque cellule de notre labyrinthe, et en reliant deux cellules séparées par une porte, on peut dessiner un arbre à partir de n'importe quel labyrinthe.
Sur notre graphe, un arête est donc un passage dans notre labyrinthe.
Génération
Un labyrinthe rectangulaire de largeur et de hauteur contient exactement portes.
Du point de vue du graphe, il y a donc arêtes.
Maintenant, comment répartir toutes ces portes de façon à ce que toutes les cellules soient accessibles et qu'il n'existe qu'un unique chemin entre l'entrée et la sortie ?
De nombreux algorithmes existent, dont le parcours en profondeur et l'algorithme de Kruskal. Chaque algorithme est différent et produit des labyrinthes visuellement différents.
Parcours en profondeur
Dans l'algorithme du parcours en profondeur ("recursive backtracker" en anglais), on commence sur une cellule aléatoire dans le labyrinthe.
On se dirige dans une direction aléatoire et on casse le mur en face, tout en marquant la cellule précédente comme visitée.
Lorsque aucune direction n'est disponible, on remonte à la position précédente.
Pour sauvegarder l'historique des positions, on peut utiliser une pile.
Une pile est une liste dans laquelle seul le dernier élément peut être récupéré.
Une pile contient 2 méthodes :
- push(elem) : ajoute elem à la pile
- pop() -> elem : retourne le dernier élément de la pile (elem dans notre cas)
Dans mon implémentation de la pile, une erreur est levée si l'on appelle la méthode pop sur une pile vide.
Kruskal
Alors que pour le parcours en profondeur on partait des cellules, l'algorithme de Kruskal s'applique sur des murs.
On attribue à chaque cellule un identifiant unique.
On choisit un mur aléatoire. Si les cellules séparées par ce mur ont des identifiants différents, on leur associe le même identifiant et on casse le mur.
Sinon, on passe à un autre mur.
Pour mettre en œuvre cet algorithme, on utilise une structure de données appelée Union-Find, qui permet de mettre en relation des éléments partageant un même représentant (ou un même identifiant).
Il existe plusieurs variantes d'Union-Find, dont notamment la compression de chemin, qui permet de n'avoir qu'un ou deux éléments à parcourir pour obtenir le représentant d'un élément.
Union-Find fonctionne comme un arbre. 'a' a un représentant 'A', etc..., qui lui même a un représentant '', qui est son propre représentant.
Union-Find est composé de deux méthodes :
- union(a, b) : associe à 'a' et 'b' le même représentant, par exemple 'A' le représentant de 'a'
- find(a) -> : tant que l'élément n'est pas son propre représentant, on applique find au représentant de l'élément.
On commence donc par find(a), puis find(A) (si 'A' est le représentant de 'a'), etc... La méthode de la compression de chemin permet d'éviter trop de récursivité en écrivant : représentant de 'a' =
Labyrinthes cycliques
Il est possible, en retirant un certain pourcentage de murs, de créer des labyrinthes cycliques à partir des labyrinthes acycliques générés auparavant.
Cependant, certains algorithmes de résolution n'adopteront pas le même comportement.
Comparaison
Si nous comparons un labyrinthe généré avec l'algorithme du parcours en profondeur à un labyrinthe généré avec l'algorithme de Kruskal, bien qu'ils aient tous les deux une solution unique, ils apparaissent néanmoins différents visuellement.
Résolution
Il existe là encore de nombreux algorithmes de résolution de labyrinthes, plus ou moins efficaces, et surtout ne fonctionnant pas tous dans les mêmes conditions.
Parcours en profondeur
Cet algorithme est quasiment identique à celui du parcours en profondeur pour la génération.
Il est très efficace et trouvera une solution en minimisant le nombre de cases visitées.
Il s'adapte aux labyrinthes cycliques. En cas de boucle, il ne fera pas la boucle vu que l'intersection a déjà été visitées, il reviendra sur ses pas.
Il s'adapte aux labyrinthes sans solution. En effet, s'il n'y a pas de sortie, il va revenir à sa position de départ sans position précédente (pile vide) et en ayant parcouru toutes les cases.
Mur droit
Cet algorithme va suivre de la main droite le mur de droite.
Il est relativement inefficace, et va visiter un grand nombre de cases.
Il s'adapte assez mal aux labyrinthes cycliques. En effet, si la sortie n'est pas dans la même boucle que l'entrée, il ne la trouvera jamais.
Il ne s'adapte pas aux labyrinthes sans solution, et fera le tour de la grille à l'infini.
On remarquera que des deux algorithmes, celui du mur droit explorera toutes les cellules situées en dessous du chemin optimal. Il sera donc plus efficace lorsque le chemin de solution est vers le bas de la grille.
Bilan personnel
Ce projet m'a permis de découvrir plusieurs structures de données qui me seront utiles à l'avenir.
J'ai aussi pu me familiariser avec tkinter, avec la programmation orientée objets en Python (notamment héritage et méthodes statiques) et avec le multithreading (j'avais codé mon projet en programmation parallèle avant de le refaire entièrement un un seul thread une semaine avant l'oral).
J'ai aussi pu comprendre la complexité mathématique derrière un labyrinthe.
Code source
Mon projet total contenant plusieurs milliers de lignes de code réparties dans une dizaine de fichiers, je ne peux évidemment pas l'insérer directement dans cette page.
Le code source est disponible sur Github à cette adresse
Il suffit d'avoir installé Python 3 et tkinter.
Sources
Génération
Résolution
Structures de données
Python