Я хотів би додати ще одну відповідь, окрім своєї першої відповіді . Ця відповідь намагається мінімізувати кількість дзвінків на rand5()
один дзвінок rand7()
, щоб максимально використовувати випадковість. Тобто, якщо ви розглядаєте випадковість як дорогоцінний ресурс, ми хочемо використовувати якомога більше його, не відкидаючи жодних випадкових біт. Ця відповідь також має деякі подібності з логікою, представленою в відповіді Івана .
Ентропія випадкової величини є добре визначеною величиною. Для випадкової змінної, яка приймає N станів з однаковими ймовірностями (рівномірний розподіл), ентропія являє собою log 2 N. Таким чином, rand5()
має приблизно 2.32193 біт ентропії та rand7()
має приблизно 2.80735 біт ентропії. Якщо ми сподіваємося максимально використати випадковість, нам потрібно використовувати всі 2.32193 біти ентропії від кожного виклику до rand5()
та застосувати їх для отримання 2.80735 біт ентропії, необхідних для кожного дзвінка rand7()
. Принциповим обмеженням є те, що ми можемо робити не краще, ніж log (7) / log (5) = 1.20906 викликівrand5()
один виклик rand7()
.
Бічні примітки: усі логарифми у цій відповіді будуть базою 2, якщо не вказано інше. rand5()
буде прийнято повертати числа в діапазоні [0, 4], і rand7()
буде прийнято повертати числа в діапазоні [0, 6]. Регулювання діапазонів у [1, 5] та [1, 7] відповідно тривіально.
То як нам це зробити? Ми генеруємо нескінченно точне випадкове дійсне число між 0 і 1 (зробимо вигляд, що ми могли насправді обчислити і зберегти таке нескінченно точне число - це ми виправимо пізніше). Ми можемо генерувати таке число, генеруючи його цифри в базі 5: ми вибираємо випадкове число 0. a
1 a
2 a
3 ..., де кожна цифра а i
вибирається викликом до rand5()
. Наприклад, якщо наш RNG обрав a i
= 1 для всіх i
, то ігноруючи той факт, що це не дуже випадково, це відповідало б дійсному номеру 1/5 + 1/5 2 + 1/5 3 + ... = 1/4 (сума геометричного ряду).
Гаразд, тому ми вибрали випадкове дійсне число між 0 і 1. Зараз я стверджую, що таке випадкове число розподілено рівномірно. Інтуїтивно це зрозуміти легко, оскільки кожна цифра була підібрана рівномірно, а число нескінченно точне. Однак формальне підтвердження цього дещо більше, оскільки зараз ми маємо справу з безперервним розподілом замість дискретного розподілу, тому нам потрібно довести, що ймовірність того, що наше число лежить в інтервалі [a
, b
], дорівнює довжині цей інтервал,b - a
. Доведення залишається як вправа для читача =).
Тепер, коли у нас є випадкове дійсне число, вибране рівномірно з діапазону [0, 1], нам потрібно перетворити його в ряд рівномірно випадкових чисел у діапазоні [0, 6], щоб генерувати вихід rand7()
. Як ми це робимо? Просто зворотний результат того, що ми тільки що зробили - ми перетворюємо його в нескінченно точний десятковий знак у базі 7, і тоді кожна 7-цифрова база буде відповідати одному виходуrand7()
.
Беручи приклад з попереднього, якщо наш rand5()
виробляють нескінченний потік 1, то наше випадкове дійсне число буде 1/4. Перетворивши 1/4 в базу 7, ми отримаємо нескінченний десятковий 0,15151515 ..., тому ми отримаємо як вихід 1, 5, 1, 5, 1, 5 і т.д.
Гаразд, у нас є основна ідея, але у нас залишилися дві проблеми: ми насправді не можемо обчислити або зберегти нескінченно точне дійсне число, так як ми маємо справу з лише кінцевою його частиною? По-друге, як насправді перетворити його на базу 7?
Один із способів перетворення числа між 0 і 1 в базу 7 полягає в наступному:
- Помножте на 7
- Невід’ємною частиною результату є наступна основна 7 цифра
- Відніміть цілісну частину, залишивши лише дробову частину
- Перехід до кроку 1
Для вирішення проблеми нескінченної точності ми обчислюємо частковий результат, а також зберігаємо верхню межу того, яким може бути результат. Тобто, припустимо, ми зателефонували rand5()
двічі, і він повернувся 1 обидва рази. Поки що ми створили 0,11 (основа 5). Незалежно від решти нескінченних серій викликів, які ми rand5()
виробляємо, випадкове дійсне число, яке ми генеруємо, ніколи не буде більшим за 0,12: завжди вірно, що 0,11 ≤ 0,11xyz ... <0,12.
Отже, відслідковуючи поточне число дотепер і максимальне значення, яке воно може колись прийняти, ми перетворюємо обидва числа в базові 7. Якщо вони погоджуються на перші k
цифри, то ми можемо сміливо виводити наступні k
цифри - незалежно від того, що нескінченний потік базових 5 цифр є, вони ніколи не впливатимуть на наступні k
цифри представлення бази 7!
І ось алгоритм - щоб генерувати наступний висновок rand7()
, ми генеруємо лише стільки цифр, rand5()
скільки нам потрібно для того, щоб ми з певністю знали значення наступної цифри при перетворенні випадкового реального числа в базу 7. Ось реалізація Python із тестовим джгутом:
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)
Зверніть увагу, що rand7_gen()
повертає генератор, оскільки він має внутрішній стан, що включає перетворення числа в базу 7. Тестовий джгут викликає next(r7)
10000 разів для отримання 10000 випадкових чисел, а потім вимірює їх розподіл. Використовується лише ціла математика, тому результати точно правильні.
Також зауважте, що цифри тут стають дуже великими, дуже швидкими. Повноваження 5 і 7 швидко ростуть. Отже, продуктивність почне помітно погіршуватися після генерації безлічі випадкових чисел через арифметику біньюма. Але пам’ятайте тут, моя мета полягала в тому, щоб максимально використовувати випадкові біти, а не максимізувати продуктивність (хоча це вторинна мета).
За один проміжок цього я здійснив 12091 виклик rand5()
для 10000 дзвінків rand7()
, досягнувши мінімуму дзвінків журналу (7) / журналу (5) в середньому до 4 значущих цифр, і отриманий результат був рівномірним.
Щоб перенести цей код на мову, яка не має довільно великих цілих чисел, вам доведеться обмежувати значення pow5
та pow7
максимальне значення вашого рідного інтегрального типу - якщо вони занадто великі, то скиньте все і почніть спочатку. Це збільшить середню кількість дзвінків rand5()
на виклик rand7()
дуже незначно, але, сподіваємось, він не повинен надто збільшуватися навіть для 32- або 64-бітних цілих чисел.