Funciton , non compétitif
MISE À JOUR! Amélioration massive des performances! n = 7 se termine maintenant en moins de 10 minutes! Voir explication en bas!
C'était très amusant à écrire. Il s'agit d'un solveur de force brute pour ce problème écrit en Funciton. Quelques factoids:
- Il accepte un entier sur STDIN. Tout espace blanc superflu le casse, y compris un saut de ligne après l'entier.
- Il utilise les nombres 0 à n - 1 (pas 1 à n ).
- Il remplit la grille « en arrière », de sorte que vous obtenez une solution où la rangée du bas lit
3 2 1 0
plutôt que là où la ligne de haut lit 0 1 2 3
.
- Il affiche correctement
0
(la seule solution) pour n = 1.
- Sortie vide pour n = 2 et n = 3.
- Une fois compilé en un exe, cela prend environ 8¼ minutes pour n = 7 (c'était environ une heure avant l'amélioration des performances). Sans compilation (en utilisant l'interpréteur), cela prend environ 1,5 fois plus de temps, donc l'utilisation du compilateur en vaut la peine.
- En tant que jalon personnel, c'est la première fois que j'écris un programme Funciton complet sans d'abord l'écrire dans un langage pseudocode. Je l'ai d'abord écrit en C #.
- (Cependant, ce n'est pas la première fois que j'effectue un changement pour améliorer massivement les performances de quelque chose dans Funciton. La première fois que je l'ai fait, c'était dans la fonction factorielle. L'échange de l'ordre des opérandes de la multiplication a fait une énorme différence à cause de comment l'algorithme de multiplication fonctionne . Au cas où vous seriez curieux.)
Sans plus tarder:
┌────────────────────────────────────┐ ┌─────────────────┐
│ ┌─┴─╖ ╓───╖ ┌─┴─╖ ┌──────┐ │
│ ┌─────────────┤ · ╟─╢ Ӂ ╟─┤ · ╟───┤ │ │
│ │ ╘═╤═╝ ╙─┬─╜ ╘═╤═╝ ┌─┴─╖ │ │
│ │ └─────┴─────┘ │ ♯ ║ │ │
│ ┌─┴─╖ ╘═╤═╝ │ │
│ ┌────────────┤ · ╟───────────────────────────────┴───┐ │ │
┌─┴─╖ ┌─┴─╖ ┌────╖ ╘═╤═╝ ┌──────────┐ ┌────────┐ ┌─┴─╖│ │
│ ♭ ║ │ × ╟───┤ >> ╟───┴───┘ ┌─┴─╖ │ ┌────╖ └─┤ · ╟┴┐ │
╘═╤═╝ ╘═╤═╝ ╘══╤═╝ ┌─────┤ · ╟───────┴─┤ << ╟─┐ ╘═╤═╝ │ │
┌───────┴─────┘ ┌────╖ │ │ ╘═╤═╝ ╘══╤═╝ │ │ │ │
│ ┌─────────┤ >> ╟─┘ │ └───────┐ │ │ │ │ │
│ │ ╘══╤═╝ ┌─┴─╖ ╔═══╗ ┌─┴─╖ ┌┐ │ │ ┌─┴─╖ │ │
│ │ ┌┴┐ ┌───────┤ ♫ ║ ┌─╢ 0 ║ ┌─┤ · ╟─┤├─┤ ├─┤ Ӝ ║ │ │
│ │ ╔═══╗ └┬┘ │ ╘═╤═╝ │ ╚═╤═╝ │ ╘═╤═╝ └┘ │ │ ╘═╤═╝ │ │
│ │ ║ 1 ╟───┬┘ ┌─┴─╖ └───┘ ┌─┴─╖ │ │ │ │ │ ┌─┴─╖ │
│ │ ╚═══╝ ┌─┴─╖ │ ɓ ╟─────────────┤ ? ╟─┘ │ ┌─┴─╖ │ ├─┤ · ╟─┴─┐
│ ├─────────┤ · ╟─┐ ╘═╤═╝ ╘═╤═╝ ┌─┴────┤ + ╟─┘ │ ╘═╤═╝ │
┌─┴─╖ ┌─┴─╖ ╘═╤═╝ │ ╔═╧═╕ ╔═══╗ ┌───╖ ┌─┴─╖ ┌─┴─╖ ╘═══╝ │ │ │
┌─┤ · ╟─┤ · ╟───┐ └┐ └─╢ ├─╢ 0 ╟─┤ ⌑ ╟─┤ ? ╟─┤ · ╟──────────────┘ │ │
│ ╘═╤═╝ ╘═╤═╝ └───┐ ┌┴┐ ╚═╤═╛ ╚═╤═╝ ╘═══╝ ╘═╤═╝ ╘═╤═╝ │ │
│ │ ┌─┴─╖ ┌───╖ │ └┬┘ ┌─┴─╖ ┌─┘ │ │ │ │
│ ┌─┴───┤ · ╟─┤ Җ ╟─┘ └────┤ ? ╟─┴─┐ ┌─────────────┘ │ │
│ │ ╘═╤═╝ ╘═╤═╝ ╘═╤═╝ │ │╔════╗╔════╗ │ │
│ │ │ ┌──┴─╖ ┌┐ ┌┐ ┌─┴─╖ ┌─┴─╖ │║ 10 ║║ 32 ║ ┌─────────────────┘ │
│ │ │ │ << ╟─┤├─┬─┤├─┤ · ╟─┤ · ╟─┘╚══╤═╝╚╤═══╝ ┌──┴──┐ │
│ │ │ ╘══╤═╝ └┘ │ └┘ ╘═╤═╝ ╘═╤═╝ │ ┌─┴─╖ ┌─┴─╖ ┌─┴─╖ │
│ │ ┌─┴─╖ ┌─┴─╖ ┌─┴─╖ ┌─┴─╖ ╔═╧═╕ └─┤ ? ╟─┤ · ╟─┤ % ║ │
│ └─────┤ · ╟─┤ · ╟──┤ Ӂ ╟──┤ ɱ ╟─╢ ├───┐ ╘═╤═╝ ╘═╤═╝ ╘═╤═╝ │
│ ╘═╤═╝ ╘═╤═╝ ╘═╤═╝ ╘═══╝ ╚═╤═╛ ┌─┴─╖ ┌─┴─╖ │ └────────────────────┘
│ └─────┤ │ └───┤ ‼ ╟─┤ ‼ ║ │ ┌──────┐
│ │ │ ╘═══╝ ╘═╤═╝ │ │ ┌────┴────╖
│ │ │ ┌─┴─╖ │ │ │ str→int ║
│ │ └──────────────────────┤ · ╟───┴─┐ │ ╘════╤════╝
│ │ ┌─────────╖ ╘═╤═╝ │ ╔═╧═╗ ┌──┴──┐
│ └──────────┤ int→str ╟──────────┘ │ ║ ║ │ ┌───┴───┐
│ ╘═════════╝ │ ╚═══╝ │ │ ┌───╖ │
└───────────────────────────────────────────────────────┘ │ └─┤ × ╟─┘
┌──────────────┐ ╔═══╗ │ ╘═╤═╝
╔════╗ │ ╓───╖ ┌───╖ │ ┌───╢ 0 ║ │ ┌─┴─╖ ╔═══╗
║ −1 ║ └─╢ Ӝ ╟─┤ × ╟──┴──────┐ │ ╚═╤═╝ └───┤ Ӂ ╟─╢ 0 ║
╚═╤══╝ ╙───╜ ╘═╤═╝ │ │ ┌─┴─╖ ╘═╤═╝ ╚═══╝
┌─┴──╖ ┌┐ ┌───╖ ┌┐ ┌─┴──╖ ╔════╗ │ │ ┌─┤ ╟───────┴───────┐
│ << ╟─┤├─┤ ÷ ╟─┤├─┤ << ║ ║ −1 ║ │ │ │ └─┬─╜ ┌─┐ ┌─────┐ │
╘═╤══╝ └┘ ╘═╤═╝ └┘ ╘═╤══╝ ╚═╤══╝ │ │ │ └───┴─┘ │ ┌─┴─╖ │
│ └─┘ └──────┘ │ │ └───────────┘ ┌─┤ ? ╟─┘
└──────────────────────────────┘ ╓───╖ └───────────────┘ ╘═╤═╝
┌───────────╢ Җ ╟────────────┐ │
┌────────────────────────┴───┐ ╙───╜ │
│ ┌─┴────────────────────┐ ┌─┴─╖
┌─┴─╖ ┌─┴─╖ ┌─┴─┤ · ╟──────────────────┐
│ ♯ ║ ┌────────────────────┤ · ╟───────┐ │ ╘═╤═╝ │
╘═╤═╝ │ ╘═╤═╝ │ │ │ ┌───╖ │
┌─────┴───┘ ┌─────────────────┴─┐ ┌───┴───┐ ┌─┴─╖ ┌─┴─╖ ┌─┤ × ╟─┴─┐
│ │ ┌─┴─╖ │ ┌───┴────┤ · ╟─┤ · ╟──────────┤ ╘═╤═╝ │
│ │ ┌───╖ ┌───╖ ┌──┤ · ╟─┘ ┌─┴─┐ ╘═╤═╝ ╘═╤═╝ ┌─┴─╖ │ │
│ ┌────┴─┤ ♭ ╟─┤ × ╟──┘ ╘═╤═╝ │ ┌─┴─╖ ┌───╖└┐ ┌──┴─╖ ┌─┤ · ╟─┘ │
│ │ ╘═══╝ ╘═╤═╝ ┌───╖ │ │ │ × ╟─┤ Ӝ ╟─┴─┤ ÷% ╟─┐ │ ╘═╤═╝ ┌───╖ │
│ ┌─────┴───┐ ┌────┴───┤ Ӝ ╟─┴─┐ │ ╘═╤═╝ ╘═╤═╝ ╘══╤═╝ │ │ └───┤ Ӝ ╟─┘
│ ┌─┴─╖ ┌───╖ │ │ ┌────╖ ╘═╤═╝ │ └───┘ ┌─┴─╖ │ │ └────┐ ╘═╤═╝
│ │ × ╟─┤ Ӝ ╟─┘ └─┤ << ╟───┘ ┌─┴─╖ ┌───────┤ · ╟───┐ │ ┌─┴─╖ ┌───╖ │ │
│ ╘═╤═╝ ╘═╤═╝ ╘══╤═╝ ┌───┤ + ║ │ ╘═╤═╝ ├──┴─┤ · ╟─┤ × ╟─┘ │
└───┤ └────┐ ╔═══╗ ┌─┴─╖ ┌─┴─╖ ╘═╤═╝ │ ╔═══╗ ┌─┴─╖ ┌─┴─╖ ╘═╤═╝ ╘═╤═╝ │
┌─┴─╖ ┌────╖ │ ║ 0 ╟─┤ ? ╟─┤ = ║ ┌┴┐ │ ║ 0 ╟─┤ ? ╟─┤ = ║ │ │ ┌────╖ │
│ × ╟─┤ << ╟─┘ ╚═══╝ ╘═╤═╝ ╘═╤═╝ └┬┘ │ ╚═══╝ ╘═╤═╝ ╘═╤═╝ │ └─┤ << ╟─┘
╘═╤═╝ ╘═╤══╝ ┌┐ ┌┐ │ │ └───┘ ┌─┴─╖ ├──────┘ ╘═╤══╝
│ └────┤├──┬──┤├─┘ ├─────────────────┤ · ╟───┘ │
│ └┘┌─┴─╖└┘ │ ┌┐ ┌┐ ╘═╤═╝ ┌┐ ┌┐ │
└────────────┤ · ╟─────────┘ ┌─┤├─┬─┤├─┐ └───┤├─┬─┤├────────────┘
╘═╤═╝ │ └┘ │ └┘ │ └┘ │ └┘
└───────────────┘ │ └────────────┘
Explication de la première version
La première version a pris environ une heure pour résoudre n = 7. Ce qui suit explique principalement comment cette version lente fonctionnait. En bas, j'expliquerai quel changement j'ai fait pour le réduire à moins de 10 minutes.
Une excursion en morceaux
Ce programme a besoin de bits. Il a besoin de beaucoup de bits et il en a besoin aux bons endroits. Les programmeurs Funciton expérimentés savent déjà que si vous avez besoin de n bits, vous pouvez utiliser la formule
qui dans Funciton peut être exprimé comme
Lors de l'optimisation des performances, il m'est venu à l'esprit que je peux calculer la même valeur beaucoup plus rapidement en utilisant cette formule:
J'espère que vous me pardonnerez que je n'ai pas mis à jour tous les graphiques d'équation dans ce post en conséquence.
Maintenant, disons que vous ne voulez pas d'un bloc de bits contigu; en fait, vous voulez n bits à intervalles réguliers chaque k -ième bit, comme ceci:
LSB
↓
00000010000001000000100000010000001
└──┬──┘
k
La formule pour cela est assez simple une fois que vous le savez:
Dans le code, la fonction Ӝ
prend les valeurs n et k et calcule cette formule.
Garder une trace des numéros utilisés
Il y a n ² nombres dans la grille finale, et chaque nombre peut être n'importe laquelle des n valeurs possibles. Afin de garder une trace des nombres autorisés dans chaque cellule, nous conservons un nombre composé de n ³ bits, dans lequel un bit est défini pour indiquer qu'une valeur particulière est prise. Initialement, ce nombre est 0, évidemment.
L'algorithme commence dans le coin inférieur droit. Après avoir «deviné» que le premier nombre est un 0, nous devons garder une trace du fait que le 0 n'est plus autorisé dans aucune cellule de la même ligne, colonne et diagonale:
LSB (example n=5)
↓
10000 00000 00000 00000 10000
00000 10000 00000 00000 10000
00000 00000 10000 00000 10000
00000 00000 00000 10000 10000
10000 10000 10000 10000 10000
↑
MSB
À cette fin, nous calculons les quatre valeurs suivantes:
Ligne actuelle: nous avons besoin de n bits tous les n -ièmes bits (un par cellule), puis de la déplacer vers la ligne actuelle r , en se souvenant que chaque ligne contient n ² bits:
Colonne actuelle: nous avons besoin de n bits tous les n ²-ème bits (un par ligne), puis de la déplacer vers la colonne actuelle c , en se souvenant que chaque colonne contient n bits:
Diagonale avant: Nous avons besoin de n bits tous les ... (avez-vous fait attention? Vite, comprenez-le!) ... n ( n +1) -ème bit (bien joué!), Mais seulement si nous sommes réellement sur la diagonale avant:
Diagonale arrière: Deux choses ici. Tout d'abord, comment savoir si nous sommes sur la diagonale arrière? Mathématiquement, la condition est c = ( n - 1) - r , qui est la même que c = n + (- r - 1). Hé, ça te rappelle quelque chose? C'est vrai, c'est le complément à deux, nous pouvons donc utiliser la négation au niveau du bit (très efficace dans Funciton) au lieu de la décrémentation. Deuxièmement, la formule ci-dessus suppose que nous voulons que le bit le moins significatif soit défini, mais dans la diagonale arrière, nous ne le faisons pas, nous devons donc le décaler vers le haut de ... savez-vous? ... C'est vrai, n ( n - 1).
C'est aussi le seul où nous divisons potentiellement par 0 si n = 1. Cependant, Funciton s'en fiche. 0 ÷ 0 est juste 0, tu ne sais pas?
Dans le code, la fonction Җ
(celle du bas) prend n et un indice (à partir duquel elle calcule r et c par division et reste), calcule ces quatre valeurs et les or
s ensemble.
L'algorithme de force brute
L'algorithme de force brute est implémenté par Ӂ
(la fonction en haut). Cela prend n (la taille de la grille), l' index (où dans la grille nous plaçons actuellement un nombre), et pris (le nombre avec n ³ bits nous indiquant quels nombres nous pouvons encore placer dans chaque cellule).
Cette fonction renvoie une séquence de chaînes. Chaque chaîne est une solution complète à la grille. C'est un solveur complet; il retournerait toutes les solutions si vous le permettez, mais il les renvoie comme une séquence évaluée paresseusement.
Si l' index a atteint 0, nous avons réussi à remplir toute la grille, nous retournons donc une séquence contenant la chaîne vide (une solution unique qui ne couvre aucune des cellules). La chaîne vide est 0
, et nous utilisons la fonction de bibliothèque ⌑
pour la transformer en une séquence à un seul élément.
La vérification décrite sous l' amélioration des performances ci-dessous se produit ici.
Si l' index n'a pas encore atteint 0, nous le décrémentons de 1 pour obtenir l'index auquel nous devons maintenant placer un nombre (appelons cela ix ).
Nous utilisons ♫
pour générer une séquence paresseuse contenant les valeurs de 0 à n - 1.
Ensuite, nous utilisons ɓ
(liaison monadique) avec un lambda qui effectue les opérations suivantes dans l'ordre:
- Examinez d'abord le bit pertinent pris pour décider si le numéro est valide ici ou non. On peut placer un nombre i si et seulement s'il est pris & (1 << ( n × ix ) << i ) n'est pas déjà défini. S'il est défini, retournez
0
(séquence vide).
- Utilisez
Җ
pour calculer les bits correspondant à la ligne, la colonne et la ou les diagonales actuelles. Déplacez-le par i puis or
sur prise .
- Appel récursivement
Ӂ
pour récupérer toutes les solutions pour les cellules restantes, en lui passant la nouvelle prise et la ix décrémentée . Cela renvoie une séquence de chaînes incomplètes; chaque chaîne a ix caractères (la grille remplie jusqu'à l'index ix ).
- Utilisez
ɱ
(map) pour parcourir les solutions ainsi trouvées et utilisez ‼
pour concaténer i à la fin de chacune. Ajoutez une nouvelle ligne si l' index est un multiple de n , sinon un espace.
Générer le résultat
Le programme principal appelle Ӂ
(le forceur brut) avec n , index = n ² (rappelez-vous que nous remplissons la grille à l'envers) et pris = 0 (initialement rien n'est pris). Si le résultat est une séquence vide (aucune solution trouvée), sortez la chaîne vide. Sinon, sortez la première chaîne de la séquence. Notez que cela signifie qu'il n'évaluera que le premier élément de la séquence, c'est pourquoi le solveur ne continue pas tant qu'il n'a pas trouvé toutes les solutions.
Amélioration des performances
(Pour ceux qui ont déjà lu l'ancienne version de l'explication: le programme ne génère plus une séquence de séquences qui doit être séparément transformée en chaîne pour la sortie; il génère simplement une séquence de chaînes directement. J'ai édité l'explication en conséquence Mais ce n'était pas la principale amélioration. Ça y est.)
Sur ma machine, l'exe compilé de la première version a pris à peu près exactement 1 heure pour résoudre n = 7. Ce n'était pas dans le délai imparti de 10 minutes, donc je ne me suis pas reposé. (Eh bien, en fait, je ne me suis pas reposé parce que j'avais cette idée sur la façon d'accélérer massivement.)
L'algorithme tel que décrit ci-dessus arrête sa recherche et revient en arrière chaque fois qu'il rencontre une cellule dans laquelle tous les bits du nombre pris sont définis, indiquant que rien ne peut être mis dans cette cellule.
Cependant, l'algorithme continuera à remplir vainement la grille jusqu'à la cellule dans laquelle tous ces bits sont définis. Il serait beaucoup plus rapide si elle pouvait arrêter dès que toute cellule en encore à être rempli a déjà tous les bits mis, ce qui indique déjà que nous ne pouvons jamais résoudre le reste de la grille , peu importe ce que les numéros que nous mettons dans il. Mais comment vérifier efficacement si une cellule a ses n bits définis sans les parcourir tous?
L'astuce commence par l'ajout d'un seul bit par cellule au nombre pris . Au lieu de ce qui a été montré ci-dessus, cela ressemble maintenant à ceci:
LSB (example n=5)
↓
10000 0 00000 0 00000 0 00000 0 10000 0
00000 0 10000 0 00000 0 00000 0 10000 0
00000 0 00000 0 10000 0 00000 0 10000 0
00000 0 00000 0 00000 0 10000 0 10000 0
10000 0 10000 0 10000 0 10000 0 10000 0
↑
MSB
Au lieu de n ³, il y a maintenant n ² ( n + 1) bits dans ce nombre. La fonction qui remplit la ligne / colonne / diagonale actuelle a été modifiée en conséquence (en fait, complètement réécrite pour être honnête). Cependant, cette fonction ne remplira que n bits par cellule, donc le bit supplémentaire que nous venons d'ajouter sera toujours 0
.
Maintenant, disons que nous sommes à mi-chemin du calcul, nous venons de placer un 1
dans la cellule du milieu, et le nombre pris ressemble à ceci:
current
LSB column (example n=5)
↓ ↓
11111 0 10010 0 01101 0 11100 0 11101 0
00011 0 11110 0 01101 0 11101 0 11100 0
11111 0 11110 0[11101 0]11100 0 11100 0 ← current row
11111 0 11111 0 11111 0 11111 0 11111 0
11111 0 11111 0 11111 0 11111 0 11111 0
↑
MSB
Comme vous pouvez le voir, la cellule en haut à gauche (index 0) et la cellule au milieu à gauche (index 10) sont désormais impossibles. Comment pouvons-nous le déterminer le plus efficacement possible?
Considérez un nombre dans lequel le 0e bit de chaque cellule est défini, mais uniquement jusqu'à l'index actuel. Un tel nombre est facile à calculer en utilisant la formule familière:
Qu'obtiendrions-nous si nous additionnions ces deux chiffres ensemble?
LSB LSB
↓ ↓
11111 0 10010 0 01101 0 11100 0 11101 0 10000 0 10000 0 10000 0 10000 0 10000 0 ╓───╖
00011 0 11110 0 01101 0 11101 0 11100 0 ║ 10000 0 10000 0 10000 0 10000 0 10000 0 ║
11111 0 11110 0 11101 0 11100 0 11100 0 ═══╬═══ 10000 0 10000 0 00000 0 00000 0 00000 0 ═════ ╓─╜
11111 0 11111 0 11111 0 11111 0 11111 0 ║ 00000 0 00000 0 00000 0 00000 0 00000 0 ═════ ╨
11111 0 11111 0 11111 0 11111 0 11111 0 00000 0 00000 0 00000 0 00000 0 00000 0 o
↑ ↑
MSB MSB
Le résultat est:
OMG
↓
00000[1]01010 0 11101 0 00010 0 00011 0
10011 0 00001 0 11101 0 00011 0 00010 0
═════ 00000[1]00001 0 00011 0 11100 0 11100 0
═════ 11111 0 11111 0 11111 0 11111 0 11111 0
11111 0 11111 0 11111 0 11111 0 11111 0
Comme vous pouvez le voir, l'addition déborde dans le bit supplémentaire que nous avons ajouté, mais seulement si tous les bits de cette cellule sont définis! Par conséquent, tout ce qui reste à faire est de masquer ces bits (même formule que ci-dessus, mais << n ) et de vérifier si le résultat est 0:
00000[1]01010 0 11101 0 00010 0 00011 0 ╓╖ 00000 1 00000 1 00000 1 00000 1 00000 1 ╓─╖ ╓───╖
10011 0 00001 0 11101 0 00011 0 00010 0 ╓╜╙╖ 00000 1 00000 1 00000 1 00000 1 00000 1 ╓╜ ╙╖ ║
00000[1]00001 0 00011 0 11100 0 11100 0 ╙╥╥╜ 00000 1 00000 1 00000 0 00000 0 00000 0 ═════ ║ ║ ╓─╜
11111 0 11111 0 11111 0 11111 0 11111 0 ╓╜╙╥╜ 00000 0 00000 0 00000 0 00000 0 00000 0 ═════ ╙╖ ╓╜ ╨
11111 0 11111 0 11111 0 11111 0 11111 0 ╙──╨─ 00000 0 00000 0 00000 0 00000 0 00000 0 ╙─╜ o
S'il n'est pas nul, la grille est impossible et nous pouvons nous arrêter.
- Capture d'écran montrant la solution et le temps d'exécution pour n = 4 à 7.