diff --git a/Images/uf_example.png b/Images/uf_example.png new file mode 100644 index 0000000000000000000000000000000000000000..fe96b2244a1a228e2b135bbcf6e26b0a00a56dff Binary files /dev/null and b/Images/uf_example.png differ diff --git a/Images/uf_example.tex b/Images/uf_example.tex new file mode 100644 index 0000000000000000000000000000000000000000..15b2641d2339d36dada16866c336f62161deb725 --- /dev/null +++ b/Images/uf_example.tex @@ -0,0 +1,47 @@ +\documentclass{standalone} +\usepackage{tikz} + +\begin{document} + +\tikzset{ + ufnode/.style={ + circle, + draw, + thick + }, + ufedge/.style={ + -latex, + thick + } +} + +\begin{tikzpicture} + \draw + (0,1) node[ufnode] (n0) {0} + (1,2) node[ufnode] (n1) {1} + (2,1) node[ufnode] (n2) {2} + (5,1) node[ufnode] (n3) {3} + (6,2) node[ufnode] (n4) {4} + (7,1) node[ufnode] (n5) {5} + (7,0) node[ufnode] (n6) {6} + (10,1) node[ufnode] (n7) {7} + (10,2) node[ufnode] (n8) {8} + (13,2) node[ufnode] (n9) {9} + ; + + \draw[ufedge] (n0) -- (n1) ; + \draw[ufedge] (n2) -- (n1) ; + \draw[ufedge] (n1) edge[loop above] (n1) ; + + \draw[ufedge] (n3) -- (n4) ; + \draw[ufedge] (n5) -- (n4) ; + \draw[ufedge] (n6) -- (n5) ; + \draw[ufedge] (n4) edge[loop above] (n4) ; + + \draw[ufedge] (n7) -- (n8) ; + \draw[ufedge] (n8) edge[loop above] (n8) ; + + \draw[ufedge] (n9) edge[loop above] (n9) ; +\end{tikzpicture} + +\end{document} diff --git a/Images/uf_fusion.png b/Images/uf_fusion.png new file mode 100644 index 0000000000000000000000000000000000000000..b2f723aed62e93af2e94046aa6eaa74cf4f430fc Binary files /dev/null and b/Images/uf_fusion.png differ diff --git a/Images/uf_fusion.tex b/Images/uf_fusion.tex new file mode 100644 index 0000000000000000000000000000000000000000..55aeb55a83a747a9bbd64fc5c64c1b4910168e71 --- /dev/null +++ b/Images/uf_fusion.tex @@ -0,0 +1,56 @@ +\documentclass{standalone} +\usepackage{tikz} + +\begin{document} + +\tikzset{ + ufnode/.style={ + circle, + draw, + thick + }, + ufedge/.style={ + -latex, + thick + } +} + +\begin{tikzpicture} + \draw + (0,1) node[ufnode] (n00) {0} + (1,2) node[ufnode] (n10) {1} + (2,1) node[ufnode] (n20) {2} + (4,1) node[ufnode] (n30) {3} + (5,2) node[ufnode] (n40) {4} + (6,1) node[ufnode] (n50) {5} + (6,0) node[ufnode] (n60) {6} + + (0,1) +(10,0) node[ufnode] (n01) {0} + (1,2) +(10,0) node[ufnode] (n11) {1} + (2,1) +(10,0) node[ufnode] (n21) {2} + (4,1) +(10,0) node[ufnode] (n31) {3} + (5,2) +(10,0) node[ufnode] (n41) {4} + (6,1) +(10,0) node[ufnode] (n51) {5} + (6,0) +(10,0) node[ufnode] (n61) {6} + ; + + \draw[ufedge] (n00) -- (n10) ; + \draw[ufedge] (n20) -- (n10) ; + \draw[ufedge] (n10) -- (n40) ; + + \draw[ufedge] (n30) -- (n40) ; + \draw[ufedge] (n50) -- (n40) ; + \draw[ufedge] (n60) -- (n50) ; + \draw[ufedge] (n40) edge[loop above] (n4) ; + + \draw[ufedge] (n01) -- (n11) ; + \draw[ufedge] (n21) -- (n11) ; + \draw[ufedge] (n11) edge[loop above] (n11) ; + + \draw[ufedge] (n31) -- (n41) ; + \draw[ufedge] (n51) -- (n41) ; + \draw[ufedge] (n61) -- (n51) ; + \draw[ufedge] (n41) -- (n11) ; +\end{tikzpicture} + +\end{document} diff --git a/readme.md b/readme.md index 8a722649fbc1150f88f7fbf42496f24cd3722eb4..374a1c11b1cd2b9a08ae5fcd4cc1cab9ee480d24 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,179 @@ # Structure Union Find et labyrinthes + +Le but de ce TP est d'aborder le problème de l'Union-Find, et la structure de +données associée, et de l'appliquer pour la création de labyrinthes. + +## Union Find + +Le problème de l'union find est de gérer des ensembles disjoints d'objets : un +objet ne peut pas appartenir à deux ensembles en même temps. Initialement, tous +les objets sont dans leur propre ensemble, qui ne contient qu'eux. Il est +ensuite proposé deux opérations : + +* l'*union* permet de joindre deux ensembles ; +* la *recherche* permet de déterminer si deux objets sont dans le même ensemble. + +## Application à la création de labyrinthe + +### Un labyrinthe intéressant + +Ce TP vous propose d'appliquer la notion d'union find à la création de +labyrinthes. Cette création de labyrinthes se base sur le principe suivant : + +* le labyrinthe est créé sur une grille carrée ; +* au début chaque case de la grille est entourée de murs ; +* petit à petit des murs sont abattus pour créer le labyrinthe. + +En appliquant naïvement ce principe, il est peu probable que le labyrinthe +obtenu soit intéressant. Tout d'abord, si le nombre de murs n'est pas important, +il est peu probable que l'entrée et la sortie du labyrinthe soient accessibles +par un chemin. Ensuite ce procédé va vider le labyrinthes de ses murs sans créer +les recoins tortueux qui rendent les labyrinthes intéressants. + + + +Pour obtenir un résultat plus intéressant, nous allons donc rajouter les deux +contraintes suivantes : + +* depuis toute case, il est possible d'aller à toute autre case ; +* il n'y a qu'un seul chemin possible pour aller d'une case à une autre. + +Les labyrinthes ainsi obtenus deviennent plus intéressants. + + + +C'est pour mettre en place ces contraintes que l'Union-Find devient utile. + +### Lien entre Union-Find et labyrinthes + +Le principe est de dire que deux cases sont dans le même ensemble s'il existe un +chemin pour aller de l'une à l'autre dans le labyrinthe. Initialement, toutes +les cases sont entourées de murs, et donc chaque case est dans son propre +ensemble qui ne contient qu'elle. + +Abattre le mur entre les cases $a$ et $b$ permet de créer un chemin entre ces +deux cases. Ainsi, pour s'assurer de ne jamais créer plus d'un chemin pour +aller d'une case à une autre, il suffit de ne jamais abattre de mur séparant des +cases qui sont déjà reliées par un chemin. On utilisera donc la *recherche* pour +déterminer si deux cases sont reliées par un chemin ou non. + +En créant un chemin entre la case $a$ et la case $b$, on crée en réalité des +chemins entre toutes les cases qu'on pouvait atteindre depuis $a$ et toutes les +cases qu'on pouvait atteindre depuis $b$. On réalise donc l'*union* de +l'ensemble contenant $a$ et de l'ensemble contenant $b$. + +## Implémentation + +Ce dépôt contient un code de base pour gérer les grilles les afficher, abattre +et monter des murs. Il vous reste donc à rajouter des fichiers pour la structure +d'Union-Find et tout ce que vous trouvez utile, et à compléter le constructeur +de la classe `Labyrinthe` pour abattre des murs et créer un labyrinthe +intéressant. + +### Union-Find + +#### Arbres + +Le principe de l'Union-Find est fondé sur des arbres, mais contrairement aux +arbres binaires que vous avez déjà manipulés, ces arbres stockent les liens de +parenté de bas en haut : chaque nœud connaît son *parent*. Par convention, le +parent de la racine de l'arbre est lui-même. Chaque arbre correspond à un +ensemble disjoint. Initialement, chaque élément est donc la racine de son propre +arbre, qui ne contient que lui. + + + +Dans l'exemple ci dessus, l'Union-Find contient 4 ensembles, $0$ est l'enfant de +$1$, $1$ est la racine de son arbre. + +Du point de vue de l'implémentation, le plus simple est d'identifier les $n$ +objets par les entiers de $[0,n-1]$. Un tableau `tab` d'entiers de taille $n$ +permet de stocker dans la case $i$ le numéro du parent de la case $i$. Dans +l'exemple précédent, nous aurions donc le tableau + +``` +[1, 1, 1, 4, 4, 4, 5, 8, 8, 9] +``` + +#### Recherche + +Chaque ensemble est identifié par la racine de l'arbre qui le représente. La +recherche consiste donc à suivre la chaîne de parents d'un nœud jusqu'à +atteindre la racine de l'arbre. Pour déterminer si deux nœuds sont dans le même +ensemble, on regarde si les racines de leurs arbres sont identiques. + +#### Union + +Pour unir deux ensemble, il suffit de s'assurer que deux arbres initialement +distincts finissent avec la même racine. Il suffit donc de faire en sorte que le +parent de l'une des deux racines devienne l'autre racine. + +Dans l'exemple précédent la fusion des deux premier arbres donnerait donc l'un +des deux arbres suivants + + + +Ce qui revient à renseigner 4 comme parent de 1 + +``` +[1, 4, 1, 4, 4, 4, ...] +``` + +ou à renseigner 1 comme parent de 4 + +``` +[1, 1, 1, 4, 1, 4, ...] +``` + +### Mise en place pour les labyrinthes + +Muni·e de cette base pour l'Union-Find, il est désormais possible de l'utiliser +pour nos labyrinthes. + +#### Mélange des murs + +Pour que les labyrinthes soient intéressants, il est nécessaire d'essayer +d'abattre l'ensemble des murs dans un ordre aléatoire. La première solution +naïve serait de tirer un mur au hasard, et s'il est debout d'essayer de +l'abattre. Dans la mesure où petit à petit il ne sera plus possible d'abattre la +majorité des murs, cette solution finira par coûter très cher pour abattre les +derniers murs, et il sera difficile également de savoir quand s'arrêter. + +La bonne solution consiste à créer une petite structure de votre choix pour +représenter un mur entre deux cases, et de stocker dans un tableau tous les murs +possibles. Vous pouvez ensuite mélanger ce tableau en utilisant l'algorithme vu +en cours, ou les fonctionnalités de la librairie standard. + +Une fois le tableau mélangé, vous pouvez le parcourir linéairement, et essayer +d'abattre chacun des murs qu'il contient. Après un passage, il n'est plus +possible d'abattre le moindre mur, et le labyrinthe respecte les contraintes. + +#### Utilisation de l'Union-Find + +Pour utiliser votre Union-Find, vous pouvez numéroter implicitement toutes les +cases de la grille en disant que la case à la ligne $l$ et la colonne $c$ est +associée au numéro $l \times \mathrm{largeur} + c$. + +À chaque mur traité, récupérez les racines des arbres Union-Find des cases de +part et d'autre. Si les racines sont les mêmes, les cases sont déjà reliées par +un chemin, et le mur doit rester en place, sinon vous pouvez l'abattre, et +fusionner les ensembles des deux cases. + +### Optimisation de l'Union-Find + +L'implémentation initiale de l'Union-Find proposée peut être coûteuse en terme +de complexité. Dans le pire des cas, les fusions sont réalisées dans un ordre +tel que les arbres sont en réalité des sortes de listes chaînées, qu'il faut +parcourir d'un bout à l'autre pour en trouver la racine. La complexité dans le +pire des cas pour une recherche est donc linéaire, et la fusion nécessitant de +trouver les racines des deux arbres a la même complexité. Ces complexités +peuvent être nettement améliorées grâce à deux idées simples à mettre en œuvre. + +#### Compression de chemin + +Lorsque vous cherchez la racine d'un arbre, vous partez d'un nœud et sautez de +nœud en nœud pour la trouver. La compression de chemin fait en sorte qu'une fois +la racine trouvée, tous les nœuds sur le chemin changent de parent, pour prendre +directement la racine pour parente. + +#### Minimisation des hauteurs