Ce défi ne concerne pas le jeu Snake.
Imaginez un serpent 2D formé en dessinant une ligne horizontale de longueur n
. Aux points entiers le long de son corps, ce serpent peut faire pivoter son corps de 90 degrés. Si nous définissons l'avant du serpent comme étant à l'extrême gauche pour commencer, la rotation déplacera la partie arrière du serpent et la partie avant restera en place. En effectuant des rotations répétées, il peut créer de nombreuses formes de corps de serpent différentes.
Règles
- Une partie du corps du serpent ne peut pas en chevaucher une autre.
- Il doit être possible d'atteindre l'orientation finale sans qu'aucune partie du corps du serpent ne se chevauche. Deux points qui se touchent sont comptés comme se chevauchant dans ce problème.
- Je considère un serpent et son revers comme ayant la même forme.
Tâche
Jusqu'à la rotation, la translation et la symétrie miroir, quel est le nombre total de formes de corps de serpent différentes qui peuvent être faites?
Exemple de rotation d'une partie du corps du serpent. Imaginez n=10
et le serpent est dans son orientation de départ d'une ligne droite. Tournez maintenant au point 4
90 degrés dans le sens antihoraire. Nous obtenons le serpent de 4
à 10
(la queue du serpent) couché verticalement et le serpent de 0
à 4
couché horizontalement. Le serpent a maintenant un angle droit dans son corps.
Voici quelques exemples grâce à Martin Büttner.
Nous commençons par le serpent horizontal.
Maintenant, nous tournons de la position 4.
On se retrouve après la rotation dans cette orientation.
Considérons maintenant cette orientation d'un serpent différent.
Nous pouvons maintenant voir un mouvement illégal où il y aurait un chevauchement causé pendant la rotation.
But
Votre score est le plus élevé n
pour lequel votre code peut résoudre le problème en moins d'une minute sur mon ordinateur.
Lorsqu'une rotation se produit, elle déplace une moitié du serpent avec elle. Nous devons nous inquiéter de savoir si une partie de cette partie en rotation pourrait chevaucher une partie du serpent pendant la rotation. Pour simplifier, nous pouvons supposer que le serpent a une largeur nulle. Vous ne pouvez tourner qu'à un point particulier du serpent jusqu'à 90 degrés dans le sens horaire ou antihoraire. Car, vous ne pouvez jamais plier complètement le serpent en deux car cela aurait impliqué deux rotations au même point dans la même direction.
Formes impossibles à réaliser
Un exemple simple d'une forme qui ne peut pas être faite est une majuscule T
. Une version plus sophistiquée est
(Merci à Harald Hanche-Olsen pour cet exemple)
Dans cet exemple, toutes les lignes horizontales adjacentes sont séparées de 1, tout comme les verticales. Il n'y a donc pas de mouvement légal à partir de cette position et comme le problème est réversible il n'y a donc aucun moyen d'y arriver depuis la position de départ.
Langues et bibliothèques
Vous pouvez utiliser n'importe quelle langue disposant d'un compilateur / interprète / etc disponible gratuitement. pour Linux et toutes les bibliothèques qui sont également disponibles gratuitement pour Linux.
Ma machine Les horaires seront exécutés sur ma machine. Il s'agit d'une installation ubuntu standard sur un processeur AMD FX-8350 à huit cœurs. Cela signifie également que je dois pouvoir exécuter votre code. Par conséquent, n'utilisez que des logiciels gratuits facilement disponibles et veuillez inclure des instructions complètes sur la façon de compiler et d'exécuter votre code.
Réponses:
Python 3 - score provisoire: n = 11 (n = 13 avec PyPy *)
Puisqu'il n'y a eu aucune réponse la première semaine, voici un exemple en Python pour encourager la compétition. J'ai essayé de le rendre raisonnablement lisible afin que les inefficacités puissent être facilement identifiées pour donner des idées pour d'autres réponses.
Approche
Code
(maintenant avec quelques doctests et assert après ma première tentative incorrecte)
Résultats
Sur ma machine, le serpent le plus long qui peut être calculé en moins d'une minute est la longueur 11 (ou la longueur 13 avec PyPy *). Ce n'est évidemment qu'un score provisoire jusqu'à ce que nous découvrions le score officiel de la machine de Lembik.
À titre de comparaison, voici les résultats des premières valeurs de n:
Veuillez me faire savoir si l'un de ces éléments s'avère incorrect.
Si vous avez un exemple d'arrangement qui ne doit pas pouvoir être déplié, vous pouvez utiliser la fonction
neighbours(snake)
pour rechercher tous les arrangements accessibles en une seule étape, comme test du code.snake
est un tuple de directions communes (0 pour le sens horaire, 1 pour le droit, 2 pour le sens antihoraire). Par exemple (1,1,1) est un serpent droit de longueur 4 (avec 3 articulations).Visualisation
Pour visualiser un serpent que vous avez en tête, ou l'un des serpents retournés par
neighbours
, vous pouvez utiliser la fonctiondisplay(snake)
. Cela accepte un tuple comme les autres fonctions, mais comme il n'est pas utilisé par le programme principal et n'a donc pas besoin d'être rapide, il acceptera également une chaîne, pour votre commodité.display((1,1,2,0))
est équivalent àdisplay("1120")
Comme Lembik le mentionne dans les commentaires, mes résultats sont identiques à OEIS A037245 qui ne prend pas en compte les intersections lors de la rotation. En effet, pour les premières valeurs de n, il n'y a pas de différence - toutes les formes qui ne s'entrecoupent pas peuvent être atteintes en pliant un serpent droit. L'exactitude du code peut être testée en appelant
neighbours()
avec un serpent qui ne peut pas être déplié sans intersection. Comme il n'a pas de voisins, il ne contribuera qu'à la séquence OEIS et non à celle-ci. Le plus petit exemple que je connaisse est ce serpent de longueur 31 que Lembik a mentionné, grâce à David K :(1,1,1,1,2,1,2,1,1,1,1,1,1,2,1,1,1,2,1,1,2,2,1,0,1,0,1,1,1,1)
display('111121211111121112112210101111')
donne la sortie suivante:J'adorerais entendre quelqu'un qui a un exemple plus court sans voisin. Je soupçonne que l'exemple le plus court marquera le plus petit n pour lequel les deux séquences diffèrent.
* Notez que chaque incrément de n prend environ 3 fois plus de temps, donc passer de n = 11 à n = 13 nécessite presque 10 fois le temps. C'est pourquoi PyPy permet uniquement d'augmenter n de 2, même s'il s'exécute considérablement plus rapidement que l'interpréteur Python standard.
la source
C ++ 11 - fonctionne presque :)
Après avoir lu cet article , j'ai recueilli des morceaux de sagesse de ce gars qui a apparemment travaillé pendant 25 ans sur le problème moins compliqué de compter les chemins d'auto-évitement sur un réseau carré.
Construire l'exécutable
Compiler avec J'utilise MinGW sous Win7 avec g ++ 4.8 pour les builds "linux", donc la portabilité n'est pas garantie à 100%.
g++ -O3 -std=c++11
Il fonctionne également (en quelque sorte) avec un projet MSVC2013 standard
En définissant
NDEBUG
, vous obtenez des traces d'exécution de l'algorithme et un résumé des configurations trouvées.Les performances
avec ou sans tables de hachage, le compilateur Microsoft fonctionne misérablement: la construction g ++ est 3 fois plus rapide .
L'algorithme n'utilise pratiquement pas de mémoire.
Étant donné que le contrôle de collision est à peu près en O (n), le temps de calcul doit être en O (nk n ), avec k légèrement inférieur à 3.
Sur mon [email protected], n = 17 prend environ 1:30 (environ 2 millions serpents / minute).
Je n'ai pas fini d'optimiser, mais je ne m'attendrais pas à plus d'un gain x3, donc en gros je peux espérer atteindre peut-être n = 20 en moins d'une heure, ou n = 24 en moins d'une journée.
Atteindre la première forme inflexible connue (n = 31) prendrait entre quelques années et une décennie, en supposant qu'aucune panne de courant.
Compter les formes
A N serpent de taille a N-1 articulations.
Chaque articulation peut être laissée droite ou pliée à gauche ou à droite (3 possibilités).
Le nombre de pliages possibles est donc de 3 N-1 .
Les collisions réduiront quelque peu ce nombre, de sorte que le nombre réel est proche de 2,7 N-1
Cependant, de nombreux pliages de ce type conduisent à des formes identiques.
deux formes sont identiques s'il y a soit une rotation soit une symétrie qui peuvent se transformer l'une en l'autre.
Définissons un segment comme n'importe quelle partie droite du corps plié.
Par exemple, un serpent de taille 5 plié à la 2e articulation aurait 2 segments (un de 2 unités de long et le second de 3 unités de long).
Le premier segment sera nommé tête et la dernière queue .
Par convention, nous orientons la tête du serpent horizontalement avec le corps pointé vers la droite (comme dans la première figure OP).
Nous désignons une figure donnée avec une liste de longueurs de segments signées, avec des longueurs positives indiquant un pliage à droite et des négatives un pliage à gauche.
La longueur initiale est positive par convention.
Séparation des segments et des coudes
Si nous considérons uniquement les différentes façons dont un serpent de longueur N peut être divisé en segments, nous nous retrouvons avec une répartition identique aux compositions de N.
En utilisant le même algorithme que celui montré sur la page wiki, il est facile de générer toutes les 2 N-1 partitions possibles du serpent.
Chaque cloison générera à son tour tous les plis possibles en appliquant des plis à gauche ou à droite à tous ses joints. Un tel pliage sera appelé configuration .
Toutes les partitions possibles peuvent être représentées par un entier de N-1 bits, où chaque bit représente la présence d'un joint. Nous appellerons cet entier un générateur .
Élagage des partitions
En remarquant que plier une partition donnée de la tête vers le bas équivaut à plier la partition symétrique de la queue vers le haut, nous pouvons trouver tous les couples de partitions symétriques et éliminer une sur deux.
Le générateur d'une partition symétrique est le générateur de la partition écrit dans l'ordre inverse des bits, ce qui est trivialement facile et bon marché à détecter.
Cela éliminera près de la moitié des partitions possibles, les exceptions étant les partitions avec des générateurs "palindromiques" qui restent inchangés par inversion de bits (par exemple 00100100).
Prendre soin des symétries horizontales
Avec nos conventions (un serpent commence à pointer vers la droite), le tout premier pli appliqué à droite produira une famille de pliages qui seront symétriques horizontaux de ceux qui ne diffèrent que par le premier pli.
Si nous décidons que le premier virage sera toujours vers la droite, nous éliminons toutes les symétries horizontales d'un seul coup.
Nettoyer les palindromes
Ces deux coupes sont efficaces, mais pas suffisantes pour prendre soin de ces palindromes embêtants.
La vérification la plus approfondie dans le cas général est la suivante:
considérons une configuration C avec une partition palindromique.
Nous pourrions vérifier chaque nouvelle configuration par rapport aux 3 autres. Cependant, comme nous ne générons déjà que des configurations commençant par un virage à droite, nous n'avons qu'une seule symétrie possible à vérifier:
C'est la seule configuration que nous pouvons éventuellement dupliquer.
Éliminer les doublons sans stockage
Mon approche initiale était de stocker toutes les configurations dans une énorme table de hachage, pour éliminer les doublons en vérifiant la présence d'une configuration symétrique précédemment calculée.
Grâce à l'article précité, il est devenu clair que, comme les partitions et les pliages sont stockés sous forme de champs binaires, ils peuvent être comparés comme n'importe quelle valeur numérique.
Donc, pour éliminer un membre d'une paire symétrique, vous pouvez simplement comparer les deux éléments et garder systématiquement le plus petit (ou le plus grand, comme vous le souhaitez).
Ainsi, tester une configuration pour la duplication revient à calculer la partition symétrique, et si les deux sont identiques, le pliage. Aucune mémoire n'est nécessaire du tout.
Ordre de génération
Il est clair que la vérification des collisions sera la partie la plus longue, donc la réduction de ces calculs est un gain de temps majeur.
Une solution possible est d'avoir un "serpent ragdoll" qui commencera dans une configuration plate et sera progressivement plié, pour éviter de recalculer la géométrie entière du serpent pour chaque configuration possible.
En choisissant l'ordre dans lequel les configurations sont testées, de sorte qu'au plus un chiffon soit stocké pour chaque nombre total de joints, nous pouvons limiter le nombre d'instances à N-1.
J'utilise une analyse récursive du saké de la queue vers le bas, en ajoutant une seule articulation à chaque niveau. Ainsi, une nouvelle instance de ragdoll est construite au-dessus de la configuration parent, avec un seul virage supplémentaire.
Cela signifie que les plis sont appliqués dans un ordre séquentiel, ce qui semble être suffisant pour éviter les auto-collisions dans presque tous les cas.
Lorsque l'auto-collision est détectée, les virages qui conduisent au mouvement offensant sont appliqués dans tous les ordres possibles jusqu'à ce que le repli légitime soit trouvé ou que toutes les combinaisons soient épuisées.
Contrôle statique
Avant même de penser aux pièces mobiles, j'ai trouvé plus efficace de tester la forme finale statique d'un serpent pour les auto-intersections.
Cela se fait en dessinant le serpent sur une grille. Chaque point possible est tracé de la tête vers le bas. S'il y a une auto-intersection, au moins une paire de points tombera au même endroit. Cela nécessite exactement N tracés pour toute configuration de serpent, pour un temps O (N) constant.
Le principal avantage de cette approche est que le test statique à lui seul sélectionnera simplement des chemins valables auto-évitables sur un réseau carré, ce qui permet de tester l'ensemble de l'algorithme en inhibant la détection de collision dynamique et en nous assurant de trouver le nombre correct de ces chemins.
Contrôle dynamique
Lorsqu'un serpent se replie autour d'une articulation, chaque segment pivoté balaie une zone dont la forme est tout sauf anodine.
De toute évidence, vous pouvez vérifier les collisions en testant l'inclusion dans toutes ces zones balayées individuellement. Une vérification globale serait plus efficace, mais compte tenu de la complexité des zones, je ne peux penser à aucune (sauf peut-être à l'aide d'un GPU pour dessiner toutes les zones et effectuer une vérification globale).
Étant donné que le test statique prend en charge les positions de début et de fin de chaque segment, il nous suffit de vérifier les intersections avec les arcs balayés par chaque segment en rotation.
Après une discussion intéressante avec trichoplax et un peu de JavaScript pour me repérer, j'ai trouvé cette méthode:
Pour essayer de le dire en quelques mots, si vous appelez
(source: free.fr )
Pour tout segment ne contenant pas de I , la zone balayée est délimitée par 2 arcs (et 2 segments déjà pris en charge par le contrôle statique).
Si je tombe dans le segment, l'arc balayé par I doit également être pris en compte.
Cela signifie que nous pouvons comparer chaque segment immobile contre chaque segment rotatif avec 2 ou 3 intersections segment-avec-arc
J'ai utilisé la géométrie vectorielle pour éviter complètement les fonctions trigonométriques.
Les opérations vectorielles produisent du code compact et (relativement) lisible.
L'intersection segment-arc nécessite un vecteur à virgule flottante, mais la logique doit être à l'abri des erreurs d'arrondi.
J'ai trouvé cette solution élégante et efficace dans un post obscur sur le forum. Je me demande pourquoi il n'est pas plus largement diffusé.
Est-ce que ça marche?
L'inhibition de la détection de collision dynamique produit le nombre de chemins auto-évitants corrects jusqu'à n = 19, donc je suis assez confiant que la disposition globale fonctionne.
La détection de collision dynamique produit des résultats cohérents, bien que la vérification des virages dans un ordre différent soit manquante (pour l'instant).
Par conséquent, le programme compte les serpents qui peuvent être courbés de la tête vers le bas (c'est-à-dire avec les articulations repliées par ordre croissant de distance de la tête).
la source