Je voudrais ajouter une autre réponse, en plus de ma première réponse . Cette réponse tente de réduire au minimum le nombre d'appels à rand5()
par appel à rand7()
pour maximiser l'utilisation de l'aléatoire. En d'autres termes, si vous considérez le caractère aléatoire comme une ressource précieuse, nous voulons en utiliser autant que possible, sans jeter aucun bit aléatoire. Cette réponse présente également des similitudes avec la logique présentée dans le document suivant La réponse d'Ivan .
Le site entropie d'une variable aléatoire est une quantité bien définie. Pour une variable aléatoire qui prend N états avec des probabilités égales (une distribution uniforme), l'entropie est égale à log 2 N. Ainsi, rand5()
a approximativement 2,32193 bits d'entropie, et rand7()
a environ 2,80735 bits d'entropie. Si nous espérons maximiser notre utilisation de l'aléatoire, nous devons utiliser les 2,32193 bits d'entropie de chaque appel à rand5()
et les appliquer à la génération de 2,80735 bits d'entropie nécessaires pour chaque appel à rand7()
. La limite fondamentale est donc que nous ne pouvons pas faire mieux que log(7)/log(5) = 1,20906 appels pour rand5()
par appel à rand7()
.
Remarques : tous les logarithmes dans cette réponse seront en base 2, sauf indication contraire. rand5()
sera supposé retourner des nombres dans la plage [0, 4], et rand7()
sera supposé renvoyer des nombres dans la plage [0, 6]. Ajuster les plages à [1, 5] et [1, 7] respectivement est trivial.
Alors comment faisons-nous ? Nous générons un infiniment précis un nombre réel aléatoire compris entre 0 et 1 (supposons pour l'instant que nous puissions réellement calculer et stocker un tel nombre infiniment précis - nous y reviendrons plus tard). Nous pouvons générer un tel nombre en générant ses chiffres en base 5 : nous choisissons le nombre aléatoire 0. a
1 a
2 a
3 ..., où chaque chiffre a <code>i</code> est choisi par un appel à rand5()
. Par exemple, si notre RNG a choisi un <code>i</code> = 1 pour tous les i
puis, en ignorant le fait que ce n'est pas très aléatoire, cela correspondrait au nombre réel 1/5 + 1/5. 2 + 1/5 3 + ... = 1/4 (somme d'une série géométrique).
Nous avons donc choisi un nombre réel aléatoire entre 0 et 1. Je prétends maintenant qu'un tel nombre aléatoire est uniformément distribué. Intuitivement, c'est facile à comprendre, puisque chaque chiffre a été choisi uniformément et que le nombre est infiniment précis. Cependant, une preuve formelle de cette affirmation est un peu plus complexe, car nous avons maintenant affaire à une distribution continue au lieu d'une distribution discrète, et nous devons donc prouver que la probabilité que notre nombre se trouve dans un intervalle [ a
, b
] est égal à la longueur de cet intervalle, b - a
. La preuve est laissée comme un exercice pour le lecteur =).
Maintenant que nous avons un nombre réel aléatoire choisi uniformément dans la plage [0, 1], nous devons le convertir en une série de nombres uniformément aléatoires dans la plage [0, 6] pour générer la sortie de rand7()
. Comment faisons-nous cela ? C'est juste l'inverse de ce que nous venons de faire - nous le convertissons en une décimale infiniment précise en base 7, et alors chaque chiffre en base 7 correspondra à une sortie de rand7()
.
En reprenant l'exemple précédent, si notre rand5()
produit un flux infini de 1, alors notre nombre réel aléatoire sera 1/4. En convertissant 1/4 en base 7, nous obtenons la décimale infinie 0,15151515..., nous produirons donc en sortie 1, 5, 1, 5, 1, 5, etc.
Ok, nous avons donc l'idée principale, mais il nous reste deux problèmes : nous ne pouvons pas calculer ou stocker un nombre réel infiniment précis, alors comment faire avec seulement une partie finie de celui-ci ? Deuxièmement, comment le convertir en base 7 ?
Une façon de convertir un nombre entre 0 et 1 en base 7 est la suivante :
- Multiplier par 7
- La partie intégrale du résultat est le chiffre suivant en base 7
- Soustrayez la partie intégrale, ne laissant que la partie fractionnaire.
- Sauter l'étape 1
Pour résoudre le problème de la précision infinie, nous calculons un résultat partiel et nous stockons également une limite supérieure sur ce que le résultat pourrait être. En d'autres termes, supposons que nous ayons appelé rand5()
deux fois et il a retourné 1 les deux fois. Le nombre que nous avons généré jusqu'à présent est 0,11 (base 5). Quel que soit le reste de la série infinie d'appels à rand5()
produire, le nombre réel aléatoire que nous générons ne sera jamais supérieur à 0,12 : il est toujours vrai que 0,11 ≤ 0,11xyz.... < 0.12.
Donc, en gardant à l'esprit le nombre actuel jusqu'à présent, et la valeur maximale qu'il pourrait jamais prendre, nous convertissons les deux les nombres en base 7. S'ils sont d'accord sur le premier k
chiffres, alors nous pouvons sans risque sortir le prochain k
chiffres -- quel que soit le flux infini de chiffres en base 5, ils n'affecteront jamais le prochain k
chiffres de la représentation en base 7 !
Et c'est l'algorithme -- pour générer la prochaine sortie de rand7()
nous ne générons qu'autant de chiffres de rand5()
car nous devons nous assurer que nous connaissons avec certitude la valeur du chiffre suivant dans la conversion du nombre réel aléatoire en base 7. Voici une implémentation Python, avec un harnais de tests :
import random
rand5_calls = 0
def rand5():
global rand5_calls
rand5_calls += 1
return random.randint(0, 4)
def rand7_gen():
state = 0
pow5 = 1
pow7 = 7
while True:
if state / pow5 == (state + pow7) / pow5:
result = state / pow5
state = (state - result * pow5) * 7
pow7 *= 7
yield result
else:
state = 5 * state + pow7 * rand5()
pow5 *= 5
if __name__ == '__main__':
r7 = rand7_gen()
N = 10000
x = list(next(r7) for i in range(N))
distr = [x.count(i) for i in range(7)]
expmean = N / 7.0
expstddev = math.sqrt(N * (1.0/7.0) * (6.0/7.0))
print '%d TRIALS' % N
print 'Expected mean: %.1f' % expmean
print 'Expected standard deviation: %.1f' % expstddev
print
print 'DISTRIBUTION:'
for i in range(7):
print '%d: %d (%+.3f stddevs)' % (i, distr[i], (distr[i] - expmean) / expstddev)
print
print 'Calls to rand5: %d (average of %f per call to rand7)' % (rand5_calls, float(rand5_calls) / N)
Notez que rand7_gen()
renvoie un générateur, puisqu'il possède un état interne impliquant la conversion du nombre en base 7. Le harnais de test appelle next(r7)
10000 fois pour produire 10000 nombres aléatoires, puis il mesure leur distribution. Seuls des calculs en nombres entiers sont utilisés, de sorte que les résultats sont exactement corrects.
Notez aussi que les chiffres ici sont très grand, très rapide. Les puissances de 5 et 7 croissent rapidement. Par conséquent, les performances commenceront à se dégrader sensiblement après la génération de nombreux nombres aléatoires, en raison de l'arithmétique du bignum. Mais n'oubliez pas que mon objectif était de maximiser l'utilisation des bits aléatoires, et non de maximiser les performances (bien que ce soit un objectif secondaire).
En une seule fois, j'ai fait 12091 appels à rand5()
pour 10000 appels à rand7()
Le résultat est uniforme et le minimum de log(7)/log(5) est atteint en moyenne à 4 chiffres significatifs.
Afin de porter ce code dans un langage qui n'intègre pas d'entiers de taille arbitraire, vous devrez plafonner les valeurs des éléments suivants pow5
et pow7
à la valeur maximale de votre type d'intégrale native -- s'ils deviennent trop grands, alors remettez tout à zéro et recommencez. Cela augmentera le nombre moyen d'appels à rand5()
par appel à rand7()
très légèrement, mais avec un peu de chance, elle ne devrait pas trop augmenter, même pour les entiers 32 ou 64 bits.