C99 - Carte 3x3 en 0,084 s
Edit: j'ai refactorisé mon code et fait une analyse plus approfondie des résultats.
Modifications supplémentaires: ajout de l'élagage par symétrie. Cela fait 4 configurations d'algorithmes: avec ou sans symétries X avec ou sans élagage alpha-bêta
Modifications les plus éloignées: Ajout de la mémorisation à l'aide d'une table de hachage, réalisant enfin l'impossible: résoudre une carte 3x3!
Caractéristiques principales:
- mise en œuvre simple de minimax avec élagage alpha-bêta
- très peu de gestion de la mémoire (maintient la DLL des mouvements valides; O (1) mises à jour par branche dans la recherche dans l'arborescence)
- deuxième fichier avec élagage par symétrie. Réalise toujours O (1) mises à jour par branche (techniquement O (S) où S est le nombre de symétries. Il s'agit de 7 pour les panneaux carrés et 3 pour les panneaux non carrés)
- les troisième et quatrième fichiers ajoutent la mémorisation. Vous contrôlez la taille de la table de hachage (
#define HASHTABLE_BITWIDTH
). Lorsque cette taille est supérieure ou égale au nombre de murs, elle ne garantit aucune collision ni mise à jour O (1). Les petites tables de hachage auront plus de collisions et seront légèrement plus lentes.
- compiler avec
-DDEBUG
pour les impressions
Améliorations potentielles:
correction d'une petite fuite de mémoire corrigée lors de la première modification
élagage alpha / bêta ajouté dans la 2e édition
élaguer les symétries ajoutées dans la troisième édition (notez que les symétries ne sont pas gérées par la mémorisation, ce qui reste une optimisation distincte.)
mémorisation ajoutée dans la 4e édition
- actuellement la mémorisation utilise un bit indicateur pour chaque mur. Une planche 3x4 a 31 murs, donc cette méthode ne pouvait pas gérer les planches 4x4 indépendamment des contraintes de temps. l'amélioration consisterait à émuler des entiers X bits, où X est au moins aussi grand que le nombre de murs.
Code
Faute d'organisation, le nombre de fichiers est devenu incontrôlable. Tout le code a été déplacé vers ce référentiel Github . Dans l'édition de mémorisation, j'ai ajouté un makefile et un script de test.
Résultats
Notes sur la complexité
Les approches par force brute des points et des boîtes explosent très rapidement en complexité .
Prenons un tableau avec des R
lignes et des C
colonnes. Il y a des R*C
carrés, R*(C+1)
des murs verticaux et des C*(R+1)
murs horizontaux. C'est un total de W = 2*R*C + R + C
.
Parce que Lembik nous a demandé de résoudre le jeu avec minimax, nous devons traverser jusqu'aux feuilles de l'arbre à gibier. Ignorons pour l'instant la taille, car ce qui compte, ce sont les ordres de grandeur.
Il existe des W
options pour le premier coup. Pour chacun d'eux, le joueur suivant peut jouer n'importe lequel des W-1
murs restants, etc. Cela nous donne un espace de recherche de SS = W * (W-1) * (W-2) * ... * 1
, ou SS = W!
. Les factoriels sont énormes, mais ce n'est que le début. SS
est le nombre de nœuds feuilles dans l'espace de recherche. Plus pertinent pour notre analyse est le nombre total de décisions qui ont dû être prises (c'est-à-dire le nombre de branches B
dans l'arbre). La première couche de branches a des W
options. Pour chacun d'eux, le niveau suivant a W-1
, etc.
B = W + W*(W-1) + W*(W-1)*(W-2) + ... + W!
B = SUM W!/(W-k)!
k=0..W-1
Regardons quelques petites tailles de table:
Board Size Walls Leaves (SS) Branches (B)
---------------------------------------------------
1x1 04 24 64
1x2 07 5040 13699
2x2 12 479001600 1302061344
2x3 17 355687428096000 966858672404689
Ces chiffres deviennent ridicules. Au moins, ils expliquent pourquoi le code de force brute semble se bloquer indéfiniment sur une carte 2x3. L'espace de recherche d'une carte 2x3 est 742560 fois plus grand que 2x2 . Si 2x2 prend 20 secondes, une extrapolation prudente prédit plus de 100 jours de temps d'exécution pour 2x3. De toute évidence, nous devons tailler.
Analyse d'élagage
J'ai commencé par ajouter un élagage très simple en utilisant l'algorithme alpha-bêta. Fondamentalement, il arrête de chercher si un adversaire idéal ne lui donnerait jamais ses opportunités actuelles. "Hé, regarde - je gagne beaucoup si mon adversaire me laisse gagner chaque case!", Ne pensait jamais à l'IA.
edit J'ai également ajouté un élagage basé sur des planches symétriques. Je n'utilise pas une approche de mémorisation, juste au cas où un jour j'ajouterais la mémorisation et que je voudrais garder cette analyse séparée. Au lieu de cela, cela fonctionne comme ceci: la plupart des lignes ont une "paire symétrique" ailleurs sur la grille. Il existe jusqu'à 7 symétries (horizontale, verticale, rotation de 180, rotation de 90, rotation de 270, diagonale et l'autre diagonale). Les 7 s'appliquent aux planches carrées, mais les 4 dernières ne s'appliquent pas aux planches non carrées. Chaque mur a un pointeur sur sa "paire" pour chacune de ces symétries. Si, dans un tour, le plateau est symétrique horizontalement, alors une seule de chaque paire horizontale doit être jouée.
modifier modifier Mémorisation! Chaque mur reçoit un identifiant unique, que j'ai commodément défini comme un bit indicateur; le nième mur a l'identifiant 1 << n
. Le hachage d'une planche n'est alors que le OU de tous les murs joués. Ceci est mis à jour à chaque branche en temps O (1). La taille de la table de hachage est définie dans a #define
. Tous les tests ont été exécutés avec une taille 2 ^ 12, car pourquoi pas? Lorsqu'il y a plus de murs que de bits indexant la table de hachage (12 bits dans ce cas), les 12 moins significatifs sont masqués et utilisés comme index. Les collisions sont gérées avec une liste chaînée à chaque index de table de hachage. Le graphique suivant est mon analyse rapide et sale de la façon dont la taille de la table de hachage affecte les performances. Sur un ordinateur avec une RAM infinie, nous définirions toujours la taille de la table au nombre de murs. Une carte 3x4 aurait une table de hachage de 2 ^ 31 de long. Hélas, nous n'avons pas ce luxe.
Ok, revenons à l'élagage. En arrêtant la recherche en haut de l'arbre, on peut gagner beaucoup de temps en ne descendant pas vers les feuilles. Le «facteur d'élagage» est la fraction de toutes les branches possibles que nous avons dû visiter. La force brute a un facteur d'élagage de 1. Plus elle est petite, mieux c'est.