Rendu ESPERON DEPRESLE I) Séance 1 Le principe de notre code est le suivant : Un module pur "Brute Force" Un module pur "Branch and Bound" 1) Brute Force Le module Brute Force n'est pas fait pour être efficace, ni être executé. C'est un concept fonctionnel mais extrêmement lent. Une façon efficace de le faire trouver une premier solution serait de ne pas lui faire commencer par une liste parfaitement ordonnée, étant donné que prendre cette liste en premier éloigne naturellement la première solution trouvée d'un enorme nombre de permutations 2) Branch and Bound Notre module Branch and Bound est tout à fait classique, et consiste à partir d'un constat simple : On ne va pas tester toutes les valeurs d'une sous-permutation à partir du moment ou les valeurs fixées de celles-ci ne remplissent pas la condition de
l'invariant. On fait donc un maximum de tests au fur et à mesure pour éviter de descendre dans les branches de l'arbre des permutations qui sont inutiles. 3) Méthode de permutation des diagonales Comme montré en cours, la méthode des diagonales 4*4 a été très simplement implémentée. Le principe correspond, sur du 4x4, a partir du carré généré par les nombres placés dans l'ordre, a permuter les elements x(n) avec x(ordre² - n - 1), soit sur les diagonales, soit sur tout sauf les diagonales. Le principe mathématique derrière cette technique est le suivant. On a les diagonales qui répondent naturellement a la contrainte de l'invariant, parce que, quelle que soit la méthode, ses membres restent les mêmes. On va poser deux concepts pour essayer d'expliquer la méthode mathématique ensuite pour les lignes et les colonnes. Je vais parler de nombre "complément bas", pour parler des nombre qui sont d'indice x(n), avec n < ordre²/2, et pour chaque complement bas, il y'a son "complement haut", qui est égal a x(ordre² - n + 1) La méthode d'inversion des valeurs va donc, pour chaque ligne, permuter deux nombres complémentaires. Cela va être fait exactement de la même façon pour les colonnes, deux permutations de nombres complémentaires.
Cela entraine dans un premier temps la présence dans chaque ligne et chaque colonnes 2 complémentaires hauts, et 2 complémentaires bas. Cela entraine aussi, grâce à l'ordre initial du carré de 4x4, le fait que les lignes soient de la forme pour n=1,13, x(n) + x(16 - (n+1) + 1 ) + x( 16 -(n+2) + 1) + x(n+3) c'est a dire 2n - 2n + 32-1 + 3 = 34 Or ce calcul n'étant plus dépendant de n, on a bien l'invariant pour ces lignes pour n = 5, 9 x(16 - (n) + 1 ) + x(n+1) + x(n+2) + x(16 - (n+3) + 1 ) Encore une fois, on a : 32-2n + 2n + 4-3 + 1 = 34 Pour les colonnes, on aura le même phénomène, que je vais détailler pour n = 1, 4 x(n) + x(16 - (n+4)+ 1) + x(16 - (n+8) + 1) + x(n + 12) Ce qui fait 2n - 2n + 32-12 + 12 + 2, c'est a dire 34. De la même facon, pour n=2,3 x(16 - (n)+ 1) + x(n+4) + x(n+8) + x(16 - (n+12) + 1) Ce qui fait 2n - 2n + 32 + 1 + 12-12 + 1 c'est a dire 34.
On peut évidemment poser ce raisonnement si et seulement si la position (n) a une valeur égale à sa position initale, c'est à dire si le tableau a été initialisé dans l'ordre... On a grâce a cette méthode de permutation donc une façon sûre de faire un carré magique sur 4x4. Cette méthode aura exactement le même résultat sur tous les multiples de 4, avec les mêmes comportements de calcul, étant donné que de la même facon, on va avoir ordre/2 compléments hauts et bas par lignes, dont les n vont s'annuler, et les additions et soustractions naturelles des lignes et colonnes génèreront l'invariant. II) Séance 2 1) Calcul du nombre de solutions possibles Nous avons tout d'abord du vérifier le nombre de solutions totales possibles pour un carré de 4x4. La plupart des ressources ne prenant pas en compte les permutations possibles pour chaque carré, nous avons donc du dénombrer tout d'abord le nombre de permutations possibles. Ce nombre est 8 pour les raisons suivantes. 1)
x o o y 2) rotation vers droite o o o x o o o y 3) rotation vers droite y o o x 4) rotation vers droite y o o o x o o o 5) symétrie verticale
o o o y o o o x 6) rotation vers droite x o o y 7) rotation vers droite x o o o y o o o 8) rotation vers droite y o o x Ce déroulement permet de générer les 8 permutations possibles,
même si il existe d'autres. Par exemple, 2 + symétrie verticale = 7, ou encore 6) + symétrie horizontale = 1) 2) Solutions évaluées Dans un premier temps, nous avons vérifié notre méthode, qui devait générer 7040, c'est à dire 8 permutations de 880 solutions. Avec la première implémentation de nos heuristiques, c'est à dire une vérification uniquement sur les lignes, colonnes et diagonales remplies, nous avions une performance d'environ 3 minutes pour la génération de toutes les solutions. Le travail principal à donc été de réduire ce temps au maximum. Nous sommes partis sur plusieurs pistes, qui vont être détaillées dans les prochaines sous-parties. 3) Génération de toutes les solutions à partir d'une solution Dans un premier temps, nous nous sommes renseignés sur l'état de l'art en matière de carrés magiques, de manière à limiter encore plus la taille de l'arbre généré, pour trouver une première solution, puis générer toutes les autres à partir de celle-ci. Visiblement, les carrés magiques 4x4 peuvent appartenir à 12 groupes différents, qui ont des propriétés différentes. Ces groupes sont classifiés vis-à-vis des positions des deux valeurs complémentaires. Ils correspondent à une généralisation
de la méthode que nous avions vue sur les diagonales. Nous pouvions donc implémenter une méthodes générant chaque possibilité de solution pour chaque groupe, puis toutes les rotations et symétries possibles à partir de chaque groupe, mais cet algo n'était plus de type branch and bound, et trop avantagé par une connaissance exhaustive des méthodes du carré 4x4. 4) Génération des 8 permutations à partir d'une solution Dans un second temps, nous nous sommes dit qu'il était possible de générer 7 solutions à partir d'une seule trouvée, en utilisant rotations et symétries comme indiqué dans le II)1). Cependant, cela sous-entend trois choses : Convenablement générer les solutions Les garder en mémoire pour ne pas les reproduire Couper intelligemment les branches situées autre part Ces trois besoins sont en fait 2 problèmes très importants : Générer les solutions n'est pas très difficile Stocker les matrices solutions en mémoire n'est pas nécéssairement extrèmement compliqué, mais parcourir ce tableau pour eventuellement chercher à couper une branche a un coût colossal Couper une branche de l'autre coté de l'arbre et pas encore
générée est littéralement impossible dans notre algorithme récursif. Ces problèmes nous ont donc poussés à laisser de côté cette méthode 5) Explication détaillée de notre méthode finale Notre méthode finale est somme toute très simple, et s'approche d'un branch and bound très pur, sans bornage de solutions. Nous partons d'une matrice vide, dans laquelle aucune valeur est insérée comme racine de l'arbre. Nous allons ensuite construire 16 fils, l'un avec 1 en première valeur fixée dans la matrice, la suivante avec 2... jusqu'a 16. Nous faisons un parcours Depth-First, nous allons donc privilégier la profondeur, juste après avoir construit la matrice 1, nous allons donc descendre dans ce fils, et construire la suite (2, 3, etc...) A partir du moment ou des vérifications peuvent-être faites, elles sont faites : A partir de deux valeurs sur une ligne, colonne ou diagonale, nous vérifions qu'il reste dans la liste des valeurs possibles des nombres assez hauts pour atteindre l'invariant A partir de 3 valeurs sur une ligne, colonne ou diagonale, nous vérifions que la somme est bien à moins de 16 de l'invariant, 16 étant la valeur maximale (Un parcours de liste ici
signifie une perte de temps). Nous vérifions aussi que la somme ne dépasse pas déjà l'invariant. A partir de 4 valeurs sur une ligne, colonne ou diagonale, nous vérifions qu'elle est bien égale à l'invariant. Ces vérifications permettent donc bien d'élaguer l'arbre au fur et à mesure pour limiter les récursions inutiles. 6) Tweaking et optimisation fine Plusieurs choses ont été testées de façon à optimiser au maximum le temps de notre algorithme. Nous avons tout d'abord testé toutes les combinaisons possibles des heuristiques listées cidessus de manière à avoir le meilleur ratio gain de temps / opération. Après un certain nombre de tests, nous en sommes arrivés à la conclusion que les tests sur 2 valeurs sont trop chers en temps pour le nombre de branches élaguées (~ 75s vs ~ 70s), nous avons donc décidé de désactiver ces vérifications. Ensuite, de manière à optimiser au maximum la rapidité, nous avons multithreadé notre algorithme : Chaque coeur du processeur va s'occuper d'une partie de l'arbre de manière à parallèliser au maximum les opérations. Il a fallu faire attention au fait d'avoir des structures de données compatibles avec ces méthodes.