Encyclopédie Atypique Incomplète
Incomplète, car toujours en construction au gré des jours, avec sérieux, curiosité et humour.
Atypique, car toujours dans l'esprit de la connaissance par l'observation et la pratique.
Incomplète, car toujours en construction au gré des jours, avec sérieux, curiosité et humour.
Atypique, car toujours dans l'esprit de la connaissance par l'observation et la pratique.
samedi 6 décembre 2008
Le terme labyrinthe vient du grec λαβύρινθος, (en latin labyrinthus), mot dont l’étymologie est douteuse...
Il désigne dans la mythologie grecque une série complexe de galeries construites par Dédale pour enfermer le Minotaure.
[1]
Par extension, un labyrinthe est un tracé sinueux, muni ou non d’embranchements, d’impasses et de fausses pistes, destiné à perdre ou à ralentir celui qui cherche à s’y déplacer.
Ce motif, apparu dès la préhistoire, se retrouve dans de très nombreuses civilisations sous des formes diverses.
De nos jours, le terme de labyrinthe désigne une organisation complexe, tortueuse, concrète (architecture, urbanisme, jardins, paysages...) ou abstraite (structures, façons de penser...), où l’homme peut se perdre.
Le cheminement du labyrinthe est difficile à suivre et à saisir dans sa globalité.
[2]
Le concept du labyrinthe a été exploité tout d’abord sous le nom de jeu de l’oie, puis sous celui de Labyrinthe. Depuis quelques années les jeux vidéos tels que Tomb Raider ou encore simplement labyrinthe exploitent ces figures complexes.
De même, le pavillon du Labyrinthe était l’une des plus spectaculaires attractions grandeur nature de l’Exposition universelle de 1967 de Montréal, et comportait des innovations de projection d’images voisines de celles qui seront mises en place bien plus tard dans le Futuroscope.
Le mythe de Dédale et du labyrinthe sont aussi un des quatre mythes fondateurs du Tarot : celui des différentes étapes du voyage initiatique vers la connaissance de soi.
À Londres, un labyrinthe de carreaux de céramique est visible sur le mur de la station de métro Warren Street (warren désigne en anglais les terriers de lapins et leurs dédales de galeries).
Aux heures de pointe, les voyageurs ont deux minutes d’attente en moyenne entre deux trains. L’auteur du dédale a estimé qu’il fallait environ trois minutes pour le résoudre.
Un labyrinthe est une surface connexe.
De telles surfaces peuvent avoir des topologies différentes : simple, ou comportant des anneaux ou îlots.
Ces deux surfaces ne sont topologiquement pas équivalentes.
Cette différence, rapportée aux labyrinthes, conduit à une distinction en deux catégories :
Les chemins des labyrinthes parfaits peuvent être représentés par des graphes directs acycliques (ou DAG) :
La recherche de chemin, couramment appelée pathfinding, est un problème de l’intelligence artificielle qui se rattache plus généralement au domaine de la planification et de la recherche de solution.
Il consiste à trouver comment se déplacer dans un environnement entre un point de départ et un point d’arrivée en prenant en compte différentes contraintes.
A la base, un problème de pathfinding peut se ramèner à un problème de recherche du meilleur chemin entre deux noeuds dans un graphe.
Sur l’illustration ci-contre, on trouve deux chemins équivalents allant de A à B, dans un environnement à deux dimensions.
Il existe un ensemble d’algorithmes classiques pour résoudre ce type de problème. Toutefois, le pathfinding devient un problème complexe lorsque l’on cherche à prendre en compte diverses contraintes additionnelles (exécution en temps réel, présence d’incertitudes, contrainte de ressources, environnement évolutif, etc).
Deux algorithmes déterministes principaux sont utilisés pour la recherche du plus court chemin dans un graphe du type labyrinthique :
Ci contre, un exemple d’exécution de l’algorithme A
Les cellules rouges forment un chemin qui part de la cellule verte vers la cellule bleue en évitant l’obstacle formé par les cellules grises.
Les nombres indiquent la distance de Manhattan à la cellule de départ en 4-connectivité.
Il existe des algorithmes qui permettent de calculer le plus court chemin de manière contrainte, par exemple par rapport à une courbure du type chemin de Dubins. Ces algorithmes sont développés pour répondre aux problèmes rencontrés en robotique vis à vis des contraintes matérielles.
On sait, de façon pragmatique, que l’on trouve obligatoirement la sortie d’un labyrinthe si on longe systématiquement un mur en le gardant, sans jamais le lâcher, à main droite ou à main gauche.
Cette idée est vérifiée dans le cas d’un labyrinthe parfait, mais elle peut conduire à explorer la totalité du labyrinthe, c’est-à-dire à passer au moins une fois dans toutes les cellules sans exception. Cela correspond à ce que l’on appelle le parcours de l’arbre en profondeur.
Cette ruse a toutefois été déjouée par les concepteurs de labyrinthes, car dans les labyrinthes à îlots, tourner systématiquement à droite, ou systématiquement à gauche, peut conduire à tourner systématiquement en rond.
L’exemple ci-dessus présente un labyrinthe à îlots imbriqués (avec son graphe abstrait associé) où la méthode de la main au mur est inopérante et où il faut passer à des méthodes plus évoluées.
La sortie d’un labyrinthe relève plus généralement de la recherche de chemin dans le graphe du labyrinthe.
On distingue deux cas :
Les algorithmes proposés ici vont s’intéresser aux labyrinthes parfaits, mais quelques adaptations permettent de créer facilement des labyrinthes à îlots.
Ils sont basés sur l’espace discrétisé dont les cellules carrées sont initialement remplies et séparées par des cloisons, selon les quatre directions (nord, sud, est et ouest).
Un labyrinthe rectangulaire parfait de m colonnes par n lignes est un ensemble de m.n cellules reliées les unes aux autres par un chemin unique.
Chaque cellule est reliée à toutes les autres, et ce, de manière unique.
L’équivalence topologique des surfaces connexes simples va nous servir pour affiner notre vision des labyrinthes parfaits. En effet, les deux surfaces connexes de la figure ci-dessous sont équivalentes : chacune a 11 cellules et 10 murs internes (marqués en pointillés).
Le nombre de murs ouverts pour permettre un chemin unique dans un labyrinthe de m.n cellules est identique au nombre de murs ouverts pour un chemin droit de m.n cellules, soit (m.n − 1) murs.
Dans un rectangle m.n cellules, le nombre total de murs internes possible est : 2.m.n − m − n.
Pour obtenir ce résultat, on compte deux murs par cellule, par exemple celui du bas et celui de droite (on évite ainsi de recompter deux fois les mêmes) et on retire le nombre de murs limitant le rectangle en bas m et à droite n.
Le nombre de murs fermés dans un labyrinthe parfait est donc de 2.m.n − m − n − (m.n − 1), soit encore (m − 1)(n − 1).
Cet algorithme, utilise une propriété des labyrinthes parfaits précédemment énoncée :
Chaque cellule est reliée à toutes les autres, et ce, de manière unique.
Il fonctionne en fusionnant progressivement des chemins depuis la simple cellule jusqu’à l’obtention d’un chemin unique, il suit donc une approche ascendante.
Au final, on obtient un chemin unique lorsque le nombre de murs ouverts atteint m.n − 1.
Ce qui donne 19 étapes dans l’exemple ci-dessous d’un labyrinthe de 5 x 4 cases :
L’historique des emplacements des cellules précédentes peut être géré de deux façons équivalentes :
La formulation récursive donne de très bon résultats pour des labyrinthes de taille modeste.
Dès lors que l’on veut générer de grands labyrinthes (1000 x 1000, par exemple), le programme risque de se terminer brutalement si la taille de la pile est insuffisante (un beau crash et un gel de l’ordinateur). Il est donc important de prévoir une taille de pile suffisante ou à défaut de passer à une autre solution comme l’historique à base de tableau.
L’exploration exhaustive est moins complexe que la fusion de chemins car elle ne nécessite pas la mise en œuvre de structures complexes.
Les deux types d’algorithmes ne sont pas tout à fait équivalents d’un point de vue qualitatif :
Pour des labyrinthes de petite taille, les deux algorithmes donneront des résultats comparables. En revanche, les grands labyrinthes auront des apparences dissemblables.
D’un point de vue quantitatif, le second type d’algorithme sera en moyenne plus rapide que le premier.
Une fois encore, sur de petites surfaces, les temps seront comparables. Mais sur de grandes surfaces, le second se montrera plus performant.
Du point de vue de la mise en œuvre, les deux algorithmes présentés sont probabilistes et à horizon fini, car le nombre de murs à enlever aléatoirement est connu.
En revanche, alors que le premier nécessite une gestion lourde de l’ensemble des murs à considérer, le second est plus simple par sa gestion de l’historique des cellules visitées, il ne requiert pas de traitements lourds et est pleinement déterministe. Il s’impose naturellement comme le meilleur choix possible.
Choix du modèle de représentation
Le labyrinthe est composé de cellules et de murs.
Ces deux concepts sont équivalents : les cellules sont limitées par des murs et les murs limitent les cellules.
Il nous faut donc choisir lequel de ces deux concepts modéliser.
Étant donné que nous parcourons les pièces (les cases du labyrinthe), il parait plus simple, à priori, de modéliser les cellules plutôt que les murs.
En première approche on pourrait comparer les ouvertures à des passages et de voir les portes comme des pointeurs (des passages) vers d’autres cellules.
Chaque cellule serait composée de 4 pointeurs : une porte serait un pointeur vers une cellule voisine, et un mur un pointeur nul.
Cette démarche fonctionne à merveille, mais si l’on regarde de plus près, nous n’en avons pas besoin.
Nous avons en effet, juste besoin de coder l’état des murs, qui sont soit ouverts, soit fermés.
Il y a 4 portes par cellule, et chaque porte n’a que deux états.
Nous n’avons donc finalement besoin que de quatre informations élémentaires par cellule.
Mais attention, il nous faudra aussi vraisemblablement savoir si nous sommes déjà passés dans une cellule. Ce qui nous rajoute une information supplémentaire par case.
Dans l’absolu, si on réfléchit un peu, cette façon de représenter les données n’est pas très correcte, dans la mesure où l’état d’une porte donnée, est stocké à 2 endroits en même temps : une information dans une cellule, et la même information dans la cellule contigüe.
Pire encore, une porte n’est vraiment ouverte que si elle est ouverte des 2 cotés dans chacune des cases !
Cependant, d’un point de vue pratique, cette redondance ne coûte que très peu (deux informations de trop par cellule), ce qui est d’autant plus négligeable que la taille minimale d’une variable est le caractère (un octet). La redondance du code ouvrant les 2 demi-portes est aussi négligeable en temps de calcul.
Choix de la structure des données
Le labyrinthe en lui même est un ensemble de m.n cellules.
Il nous faut stocker m, n et les cellules elles-mêmes.
Il faut de plus tenir compte de l’entrée et de la sortie du labyrinthe.
L’approche intuitive nous conduit à choisir un tableau de cellules pour représenter toutes les cases et les portes.
Ce n’est peut-être pas la méthode la plus économe en temps de calcul (car il faut passer par des boucles imbriquées) et en place mémoire (un tableau à deux dimensions occupe bien plus de place qu’un vecteur linéaire), mais c’est ce qu’il y a de plus simple au niveau compréhension.
Récapitulons :
Les algorithmes présentés peuvent être implémentés et programmés en n’importe quel langage procédural de haut niveau (C, C++, Java, PERL, VBA, etc.).
Le choix s’est porté ici sur du PHP, car facilement accessible et ne nécessitant pas de compilateur, linkeur, et autres usines à gaz à installer avant de pouvoir faire quoi que ce soit...
Ci-après, la définition d’une classe « labyrinthe » regroupant les deux algorithmes de création : méthode par la fusion de chemins et méthode par l’exploration exhaustive de tous les chemins.
<?php
define('S', 0); // la direction du sud
define('E', 1); // la direction de l'est
define('N', 2); // la direction du nord
define('O', 3); // la direction de l'ouest
define('IDX', 3); // le nombre de directions possibles
class labyrinthe{
protected $m; // représente la largeur du labyrinthe en cases
protected $n; // représente la hauteur du labyrinthe en cases
public function __construct($m, $n){
$this->m = $m;
$this->n = $n;
}
public function generer_algorithme_de_fusion(){
$m = $this->m;
$n = $this->n;
$laby_fusion = array();
// Initialisation du labyrithe
$k = 0;
for($i = 0; $i < $n; $i++)
{
$laby_fusion[$i] = array();
for($j = 0; $j < $m; $j++)
{
$laby_fusion[$i][$j] = array();
$laby_fusion[$i][$j][S] = 0;
$laby_fusion[$i][$j][E] = 0;
$laby_fusion[$i][$j][IDX] = $k;
$k++;
}
}
$k--; // On réajuste la variable
$i = 0;
$nb_cloison = ($m*$n)-1;
$murs = array();
for($i = 0; $i < $k; $i++)
{
if($i < ($m*($n-1)) )
$murs[S][] = $i;
if(($i+1)%$m)
{
// La case est ouvrable par le mur à droite
$murs[E][] = $i;
}
}
$directions = array(S, E);
$i = 0;
while($i < $nb_cloison)
{
do{
// On prend n'importe quelle cellule (sauf la dernière qu'on ne peut pas ouvrir)
// On peut fusionner dans 2 directions S, E
// On en prend une au hasard, si c'est impossible on prend l'autre,
// sinon on prend une autre case.
// On prend une direction au hasard
$dir = $directions[array_rand($directions)];
// Une case que l'on peut ouvrir dans cette direction
$cle = array_rand($murs[$dir]);
$id = $murs[$dir][$cle];
unset($murs[$dir][$cle]);
if(empty($murs[$dir]))
unset($directions[array_search($dir, $directions)]);
$x1 = $this->getX($id, $m);
$y1 = $this->getY($id, $m);
if($dir == S)
$id2 = $id + $m;
else
$id2 = $id +1;
$x2 = $this->getX($id2, $m);
$y2 = $this->getY($id2, $m);
$idx1 = $laby_fusion[$y1][$x1][IDX];
$idx2 = $laby_fusion[$y2][$x2][IDX];
// Tant que l'ouverture rejoint 2 fois le même groupe de cellules
}while($idx1 == $idx2);
// On ouvre les murs
$laby_fusion[$y1][$x1][$dir] = true;
// On met les deux cellules au même index
for($j = 0; $j < $n; $j++)
{
for($k = 0; $k < $m; $k++)
{
if($laby_fusion[$j][$k][IDX] == $idx2)
$laby_fusion[$j][$k][IDX] = $idx1;
}
}
$i++;
}
return $laby_fusion;
}
public function generer_algorithme_par_exploration(){
$m = $this->m;
$n = $this->n;
$laby = array();
for($i = 0; $i < $n; $i++)
{
$laby[$i] = array();
for($j = 0; $j < $m; $j++)
{
$laby[$i][$j] = array();
$laby[$i][$j][S] = 0;
$laby[$i][$j][E] = 0;
$laby[$i][$j][IDX] = 0;
}
}
// Pour etre accessible facilement
$this->laby = $laby;
$x = mt_rand(0, $m-1);
$y = mt_rand(0, $n-1);
$this->explorer( $x , $y );
return $this->laby;
}
private function explorer($x, $y){
// En premier temps on cherche les chemin possibles
// Si il n'y en a aucun on revoi, sinon on en ouvre un
$this->laby[$y][$x][IDX] = 1;
$n = $this->n;
$m = $this->m;
do{
// 4 directions possibles N,S,E,O
$directions_possibles = array(); // Directions possibles
// Au nord
if(
($y - 1) >= 0
&&
$this->laby[$y - 1][$x][IDX] == 0
)
$directions_possibles[] = N;
// Au sud
if(
($y + 1) < $n
&&
$this->laby[$y + 1][$x][IDX] == 0
)
$directions_possibles[] = S;
// A l'est
if(
($x + 1) < $m
&&
$this->laby[$y][$x + 1][IDX] == 0
)
$directions_possibles[] = E;
// A l'ouest
if(
($x - 1) >= 0
&&
$this->laby[$y][$x - 1][IDX] == 0
)
$directions_possibles[] = O;
if(empty($directions_possibles))
return;
$direction_choisie = $directions_possibles[array_rand($directions_possibles)];
if($direction_choisie == N)
{
$this->laby[$y - 1][$x][S] = true;
$this->explorer($x, $y-1);
}
elseif($direction_choisie == S)
{
$this->laby[$y][$x][S] = true;
$this->explorer($x, $y+1);
}
elseif($direction_choisie == E)
{
$this->laby[$y][$x][E] = true;
$this->explorer($x+1, $y);
}
else{
$this->laby[$y][$x - 1][E] = true;
$this->explorer($x-1, $y);
}
}while(!empty($directions_possibles));
// tant que l'on peut explorer on continue
return;
}
}
?>
Et comme un dessin vaut mieux qu’un long discours, vous pouvez jouer avec les modélisations précédentes pour obtenir de délicieuses illustrations de labyrinthes...
Nota : La méthode par « fusion de chemins » peut prendre jusqu’à plus d’une minute pour la génération d’un labyrinthe... alors soyez un peu patient avant l’obtention de l’image...
D’après un article de Yann Langlais paru dans « LinuxMag France » n°62 en juin 2004.
[1] Mosaïque romaine de Rhétie représentant le labyrinthe, Thésée et le Minotaure
[2] Labyrinthe digital situé à l’entrée de la cathédrale de Lucques (Toscane, Italie).