Ce défi est en partie un défi d'algorithmes, implique des mathématiques et est en partie simplement un défi de code le plus rapide.
Pour un entier positif n
, considérez une chaîne uniformément aléatoire de 1
s et 0
de longueur n
et appelez-la A
. Maintenant, considérons également une deuxième chaîne aléatoire de longueur uniformément choisie n
dont les valeurs sont -1
, 0,
ou 1
et appelez-la B_pre
. Maintenant , nous allons B
être B_pre
+ B_pre
. Cela est B_pre
concaténé à lui-même.
Considérez maintenant le produit intérieur de A
et B[j,...,j+n-1]
et appelez-le Z_j
et indexez à partir de 1
.
Tâche
La sortie doit être une liste de n+1
fractions. Le i
e terme de la production devrait être la exacte probabilité que tous les premiers i
termes Z_j
avec j <= i
égale 0
.
But
Le plus grand n
pour lequel votre code donne la sortie correcte en moins de 10 minutes sur ma machine.
Tie break
Si deux réponses ont le même score, celle soumise en premier l'emporte.
Dans le cas (très très) peu probable où quelqu'un trouve une méthode pour obtenir des scores illimités, la première preuve valable d'une telle solution sera acceptée.
Allusion
N'essayez pas de résoudre ce problème mathématiquement, c'est trop difficile. À mon avis, la meilleure façon est de revenir aux définitions de base des probabilités au secondaire et de trouver des façons intelligentes d'obtenir le code pour faire une énumération exhaustive des possibilités.
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.
Quelques sorties de test. Considérez juste la première sortie pour chacun n
. C'est à ce moment-là i=1
. Pour n
1 à 13, ils devraient l'être.
1: 4/6
2: 18/36
3: 88/216
4: 454/1296
5: 2424/7776
6: 13236/46656
7: 73392/279936
8: 411462/1679616
9: 2325976/10077696
10: 13233628/60466176
11: 75682512/362797056
12: 434662684/2176782336
13: 2505229744/13060694016
Vous pouvez également trouver une formule générale pour i=1
à http://oeis.org/A081671 .
Classement (divisé par langue)
- n = 15. Python + python parallèle + pypy en 1min49s par Jakube
- n = 17. C ++ en 3min37s par Keith Randall
- n = 16. C ++ en 2min38s par kuroi neko
la source
Réponses:
C ++, n = 18 en 9 min sur 8 threads
(Faites-moi savoir si cela fait moins de 10 minutes sur votre machine.)
Je profite de plusieurs formes de symétrie dans le tableau B. Ceux-ci sont cycliques (décalage d'une position), inversion (inverser l'ordre des éléments) et signe (prendre le négatif de chaque élément). Je calcule d'abord la liste des Bs que nous devons essayer et leur poids. Ensuite, chaque B est exécuté à travers une routine rapide (en utilisant les instructions de bitcount) pour les 2 ^ n valeurs de A.
Voici le résultat pour n == 18:
Compilez le programme ci-dessous avec
g++ --std=c++11 -O3 -mpopcnt dot.cc
la source
-pthread
nouveau. J'arriven=17
sur ma machine.Python 2 utilisant pypy et pp: n = 15 en 3 minutes
Aussi juste une simple force brute. Il est intéressant de voir que j'obtiens presque la même vitesse que kuroi neko avec C ++. Mon code peut atteindre
n = 12
environ 5 minutes. Et je ne l'exécute que sur un seul cœur virtuel.modifier: réduire l'espace de recherche d'un facteur de
n
J'ai remarqué qu'un vecteur cyclé
A*
deA
produit les mêmes nombres que les probabilités (mêmes nombres) que le vecteur d'origineA
lorsque je répèteB
. Par exemple, le vecteur(1, 1, 0, 1, 0, 0)
a les mêmes probabilités que chacun des vecteurs(1, 0, 1, 0, 0, 1)
,(0, 1, 0, 0, 1, 1)
,(1, 0, 0, 1, 1, 0)
,(0, 0, 1, 1, 0, 1)
et au(0, 1, 1, 0, 1, 0)
moment de choisir un échantillon aléatoireB
. Par conséquent, je n'ai pas à répéter sur chacun de ces 6 vecteurs, mais seulement environ 1 et à remplacercount[i] += 1
parcount[i] += cycle_number
.Cela réduit la complexité de
Theta(n) = 6^n
àTheta(n) = 6^n / n
. Par conséquent,n = 13
c'est environ 13 fois plus rapide que ma version précédente. Il calculen = 13
en environ 2 minutes 20 secondes. Carn = 14
c'est encore un peu trop lent. Cela prend environ 13 minutes.edit 2: Programmation multicœur
Pas vraiment satisfait de la prochaine amélioration. J'ai décidé d'essayer également d'exécuter mon programme sur plusieurs cœurs. Sur mes 2 + 2 cœurs, je peux maintenant calculer
n = 14
en environ 7 minutes. Seul un facteur d'amélioration de 2.Le code est disponible dans ce dépôt github: Lien . La programmation multicœur rend un peu moche.
edit 3: Réduction de l'espace de recherche pour les
A
vecteurs et lesB
vecteursJ'ai remarqué la même symétrie miroir pour les vecteurs
A
que kuroi neko. Je ne sais toujours pas pourquoi cela fonctionne (et si cela fonctionne pour chacunn
).La réduction de l'espace de recherche de
B
vecteurs est un peu plus intelligente. J'ai remplacé la génération des vecteurs (itertools.product
), par une fonction propre. Fondamentalement, je commence avec une liste vide et la mets sur une pile. Jusqu'à ce que la pile soit vide, je supprime une liste, si elle n'a pas la même longueur quen
, je génère 3 autres listes (en ajoutant -1, 0, 1) et en les poussant sur la pile. Je une liste a la même longueur quen
, je peux évaluer les sommes.Maintenant que je génère moi-même les vecteurs, je peux les filtrer selon que je peux atteindre la somme = 0 ou non. Par exemple, si mon vecteur
A
est(1, 1, 1, 0, 0)
, et mon vecteurB
semble(1, 1, ?, ?, ?)
, je sais, que je ne peux pas remplir les?
valeurs, de sorte queA*B = 0
. Je n'ai donc pas à parcourir tous les 6 vecteursB
de la forme(1, 1, ?, ?, ?)
.Nous pouvons améliorer cela si nous ignorons les valeurs de 1. Comme indiqué dans la question, car les valeurs de
i = 1
sont la séquence A081671 . Il existe de nombreuses façons de les calculer. Je choisis la récurrence simple:a(n) = (4*(2*n-1)*a(n-1) - 12*(n-1)*a(n-2)) / n
. Comme nous pouvons calculeri = 1
en un rien de temps, nous pouvons filtrer plus de vecteurs pourB
. Par exempleA = (0, 1, 0, 1, 1)
etB = (1, -1, ?, ?, ?)
. Nous pouvons ignorer les vecteurs, où le premier? = 1
, parce queA * cycled(B) > 0
, pour tous ces vecteurs. J'espère que vous pourrez suivre. Ce n'est probablement pas le meilleur exemple.Avec cela, je peux calculer
n = 15
en 6 minutes.modifier 4:
La grande idée de kuroi neko mise en œuvre rapidement, qui dit cela
B
et-B
produit les mêmes résultats. Accélération x2. La mise en œuvre n'est qu'un rapide hack, cependant.n = 15
en 3 minutes.Code:
Pour le code complet, visitez Github . Le code suivant n'est qu'une représentation des principales fonctionnalités. J'ai laissé de côté les importations, la programmation multicœur, l'impression des résultats, ...
Usage:
Vous devez installer pypy (pour Python 2 !!!). Le module python parallèle n'est pas porté pour Python 3. Ensuite, vous devez installer le module python parallèle pp-1.6.4.zip . Extrayez-le
cd
dans le dossier et appelezpypy setup.py install
.Ensuite, vous pouvez appeler mon programme avec
pypy you-do-the-math.py 15
Il déterminera automatiquement le nombre de processeurs. Il peut y avoir des messages d'erreur après avoir terminé le programme, ignorez-les.
n = 16
devrait être possible sur votre machine.Production:
Notes et idées:
A & B
et compter les blocs 01 et 10. Le cyclisme peut être fait en déplaçant le vecteur et en utilisant des masques, ... J'ai en fait implémenté tout cela, vous pouvez le trouver dans certaines de mes anciennes validations sur Github. Mais il s'est avéré être plus lent qu'avec les listes. Je suppose que pypy optimise vraiment les opérations de liste.la source
intimidateur laineux - C ++ - beaucoup trop lent
Eh bien, depuis qu'un meilleur programmeur a pris en charge l'implémentation C ++, j'appelle quits pour celle-ci.
Construire l'exécutable
Il s'agit d'une source C ++ 11 autonome qui se compile sans avertissements et s'exécute sans problème sur:
Si vous compilez avec g ++, utilisez: g ++ -O3 -pthread -std = c ++ 11
oublier
-pthread
produira un vidage de mémoire agréable et convivial.Optimisations
Le dernier terme Z est égal au premier (Bpre x A dans les deux cas), donc les deux derniers résultats sont toujours égaux, ce qui dispense de calculer la dernière valeur Z.
Le gain est négligeable, mais le coder ne coûte rien donc vous pouvez aussi bien l'utiliser.
Comme Jakube l'a découvert, toutes les valeurs cycliques d'un vecteur A donné produisent les mêmes probabilités.
Vous pouvez les calculer avec une seule instance de A et multiplier le résultat par le nombre de ses rotations possibles. Les groupes de rotation peuvent facilement être précalculés en un temps négligeable, il s'agit donc d'un gain de vitesse net énorme.
Étant donné que le nombre de permutations d'un vecteur de longueur n est n-1, la complexité passe de o (6 n ) à o (6 n / (n-1)), allant fondamentalement plus loin pour le même temps de calcul.
Il semble que des paires de motifs symétriques génèrent également les mêmes probabilités. Par exemple, 100101 et 101001.
Je n'ai aucune preuve mathématique de cela, mais intuitivement lorsqu'il est présenté avec tous les modèles B possibles, chaque valeur A symétrique sera alambiquée avec la valeur B symétrique correspondante pour le même résultat global.
Cela permet de regrouper quelques vecteurs A supplémentaires, pour une réduction approximative de 30% du nombre de groupes A.
FAUX
Pour une raison semi-mystérieuse, tous les motifs avec seulement un ou deux bits définis produisent le même résultat. Cela ne représente pas autant de groupes distincts, mais ils peuvent néanmoins être fusionnés pratiquement sans frais.Les vecteurs B et -B (B avec toutes les composantes multipliées par -1) produisent les mêmes probabilités.
(par exemple [1, 0, -1, 1] et [-1, 0, 1, -1]).
À l'exception du vecteur nul (toutes les composantes égales à 0), B et -B forment une paire de vecteurs distincts .
Cela permet de réduire de moitié le nombre de valeurs B en ne considérant qu'une seule de chaque paire et en multipliant sa contribution par 2, en ajoutant une seule fois la contribution globale connue de B nul à chaque probabilité.
Comment ça fonctionne
Le nombre de valeurs B est énorme (3 n ), donc leur pré-calcul nécessiterait des quantités de mémoire indécentes, ce qui ralentirait le calcul et finirait par épuiser la RAM disponible.
Malheureusement, je n'ai pas pu trouver un moyen simple d'énumérer le demi-ensemble de valeurs B optimisées, j'ai donc eu recours au codage d'un générateur dédié.
Le puissant générateur B était très amusant à coder, bien que les langages qui prennent en charge les mécanismes de rendement auraient permis de le programmer de manière beaucoup plus élégante.
Ce qu'il fait en un mot, c'est considérer le "squelette" d'un vecteur Bpre comme un vecteur binaire où les 1 représentent les valeurs réelles de -1 ou +1.
Parmi toutes ces valeurs potentielles + 1 / -1, la première est fixée à +1 (sélectionnant ainsi l'un des vecteurs B / -B possibles), et toutes les combinaisons possibles + 1 / -1 restantes sont énumérées.
Enfin, un système d'étalonnage simple garantit que chaque thread de travail traitera une plage de valeurs d'environ la même taille.
Les valeurs A sont fortement filtrées pour le regroupement en morceaux équiprobables.
Cela se fait dans une phase de pré-calcul où la force brute examine toutes les valeurs possibles.
Cette partie a un temps d'exécution O (2 n ) négligeable et n'a pas besoin d'être optimisée (le code étant déjà suffisamment lisible tel quel!).
Pour évaluer le produit interne (qui n'a besoin d'être testé que contre zéro), les composantes -1 et 1 de B sont regroupées en vecteurs binaires.
Le produit interne est nul si (et seulement si) il y a un nombre égal de + 1 et -1 parmi les valeurs B correspondant à des valeurs A non nulles.
Cela peut être calculé avec des opérations de masquage et de comptage de bits simples, aidées par
std::bitset
cela produira un code de comptage de bits raisonnablement efficace sans avoir à recourir à des instructions intrinsèques laides.Le travail est également réparti entre les cœurs, avec une affinité CPU forcée (chaque petit geste aide, ou du moins disent-ils).
Exemple de résultat
Les performances
Le multithreading devrait fonctionner parfaitement à ce sujet, bien que seuls les "vrais" cœurs contribuent pleinement à la vitesse de calcul. Mon processeur n'a que 2 cœurs pour 4 processeurs, et le gain par rapport à une version à thread unique est "seulement" d'environ 3,5.
Compilateurs
Un premier problème avec le multithreading m'a amené à croire que les compilateurs GNU fonctionnaient moins bien que Microsoft.
Après des tests plus approfondis, il semble que g ++ gagne encore une fois la journée, produisant un code environ 30% plus rapide (le même ratio que j'ai remarqué sur deux autres projets lourds en calcul).
Notamment, la
std::bitset
bibliothèque est implémentée avec des instructions de comptage de bits dédiées par g ++ 4.8, tandis que MSVC 2013 utilise uniquement des boucles de décalages de bits conventionnels.Comme on aurait pu s'y attendre, la compilation en 32 ou 64 bits ne fait aucune différence.
Améliorations supplémentaires
J'ai remarqué quelques groupes A produisant les mêmes probabilités après toutes les opérations de réduction, mais je n'ai pas pu identifier un schéma qui permettrait de les regrouper.
Voici les paires que j'ai remarquées pour n = 11:
la source
terminate called after throwing an instance of 'std::system_error' what(): Unknown error -1 Aborted (core dumped)