Я хотел бы добавить еще один ответ, в дополнение к моему первому ответу . Этот ответ пытается минимизировать количество вызовов на rand5()
один вызов rand7()
, чтобы максимально использовать случайность. То есть, если вы считаете случайность ценным ресурсом, мы хотим использовать как можно большую ее часть, не выбрасывая случайные биты. Этот ответ также имеет некоторые сходства с логикой, представленной в ответе Ивана .
Энтропия случайной величины является хорошо определенной величиной. Для случайной величины, которая принимает N состояний с равными вероятностями (равномерное распределение), энтропия равна log 2 Н. Таким образом, она rand5()
имеет приблизительно 2,332193 бита энтропии и 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 ..., где каждая цифра a 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()
, достигнув минимума вызовов log (7) / log (5) в среднем до 4 значащих цифр, и полученный результат был равномерным.
Чтобы перенести этот код на язык, в котором нет встроенных произвольно больших целых чисел, вам нужно ограничить значения pow5
и pow7
максимальное значение вашего собственного целочисленного типа - если они становятся слишком большими, затем выполнить сброс все и начать все сначала. Это немного увеличит среднее количество вызовов на rand5()
один вызов rand7()
, но, надеюсь, оно не должно увеличиться слишком сильно даже для 32- или 64-разрядных целых чисел.