Расширить случайный диапазон от 1–5 до 1–7 - PullRequest
679 голосов
/ 26 сентября 2008

Учитывая функцию, которая производит случайное целое число в диапазоне от 1 до 5, напишите функцию, которая производит случайное целое число в диапазоне от 1 до 7.

  1. Что такое простое решение?
  2. Каково эффективное решение для уменьшения использования памяти или работы на более медленном процессоре?

Ответы [ 76 ]

559 голосов
/ 09 мая 2009

Это эквивалентно решению Адама Розенфилда, но может быть немного более понятным для некоторых читателей. Предполагается, что rand5 () - это функция, которая возвращает статистически случайное целое число в диапазоне от 1 до 5 включительно.

int rand7()
{
    int vals[5][5] = {
        { 1, 2, 3, 4, 5 },
        { 6, 7, 1, 2, 3 },
        { 4, 5, 6, 7, 1 },
        { 2, 3, 4, 5, 6 },
        { 7, 0, 0, 0, 0 }
    };

    int result = 0;
    while (result == 0)
    {
        int i = rand5();
        int j = rand5();
        result = vals[i-1][j-1];
    }
    return result;
}

Как это работает? Подумайте об этом так: представьте себе распечатку этого массива двойного размера на бумаге, прикрепление его к доске для дротиков и случайное бросание в нее дротиков. Если вы нажмете ненулевое значение, это статистически случайное значение от 1 до 7, так как есть равное число ненулевых значений на выбор. Если вы попали в ноль, просто продолжайте бросать дротик, пока не достигнете ненулевого значения. Вот что делает этот код: индексы i и j случайным образом выбирают место на доске для дротиков, и если мы не получим хорошего результата, мы продолжаем бросать дротики.

Как сказал Адам, в худшем случае это может продолжаться вечно, но статистически наихудшего случая никогда не бывает. :)

343 голосов
/ 26 сентября 2008

Не существует (абсолютно правильного) решения, которое будет выполняться за постоянное количество времени, поскольку 1/7 - это бесконечное десятичное число в базе 5. Одним из простых решений было бы использование выборки отклонения, например ::100100


int i;
do
{
  i = 5 * (rand5() - 1) + rand5();  // i is now uniformly random between 1 and 25
} while(i > 21);
// i is now uniformly random between 1 and 21
return i % 7 + 1;  // result is now uniformly random between 1 and 7

Ожидаемое время выполнения составляет 25/21 = 1,19 итераций цикла, но существует бесконечно малая вероятность зацикливания навсегда.

151 голосов
/ 21 мая 2009

Я хотел бы добавить еще один ответ, в дополнение к моему первому ответу . Этот ответ пытается минимизировать количество вызовов до rand5() за вызов до rand7(), чтобы максимально использовать случайность. То есть, если вы считаете случайность ценным ресурсом, мы хотим использовать как можно большую ее часть, не выбрасывая случайные биты. Этот ответ также имеет некоторые сходства с логикой, представленной в ответ Ивана .

Энтропия случайной величины является четко определенной величиной. Для случайной величины, которая принимает N состояний с равными вероятностями (равномерное распределение), энтропия равна log 2 N. Таким образом, 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 ..., где каждая цифра i выбирается путем вызова rand5(). Например, если наш ГСЧ выбрал 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:

  1. Умножить на 7
  2. Неотъемлемой частью результата является следующая базовая 7 цифра
  3. Вычесть неотъемлемую часть, оставив только дробную часть
  4. Перейти к шагу 1

Чтобы решить проблему бесконечной точности, мы вычисляем частичный результат, а также сохраняем верхнюю границу того, каким может быть результат. То есть предположим, что мы дважды вызывали rand5(), и он возвращал 1 оба раза. Число, которое мы сгенерировали до сих пор, составляет 0,11 (основание 5). Что бы ни производили остальные бесконечные серии вызовов rand5(), случайное действительное число, которое мы генерируем, никогда не будет больше 0,12: всегда верно, что 0,11 ≤ 0,11xyz ... <0,12. </p>

Итак, отслеживая текущее число и максимальное значение, которое оно может принять, мы конвертируем оба числа в основание 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-битных целых чисел.

36 голосов
/ 30 апреля 2009

(Я украл ответ Адама Розенфельда и заставил его работать примерно на 7% быстрее.)

Предположим, что rand5 () возвращает один из {0,1,2,3,4} с равным распределением, а цель - вернуть {0,1,2,3,4,5,6} с равным распределением.

int rand7() {
  i = 5 * rand5() + rand5();
  max = 25;
  //i is uniform among {0 ... max-1}
  while(i < max%7) {
    //i is uniform among {0 ... (max%7 - 1)}
    i *= 5;
    i += rand5(); //i is uniform {0 ... (((max%7)*5) - 1)}
    max %= 7;
    max *= 5; //once again, i is uniform among {0 ... max-1}
  }
  return(i%7);
}

Мы отслеживаем наибольшее значение, которое цикл может сделать в переменной max. Если результат до сих пор находится между max% 7 и max-1, то результат будет равномерно распределен в этом диапазоне. Если нет, мы используем остаток, который является случайным между 0 и max% 7-1, и еще один вызов rand () для создания нового числа и нового максимума. Тогда мы начнем снова.

Редактировать: Ожидайте, сколько раз вызов rand5 () будет равен x в этом уравнении:

x =  2     * 21/25
   + 3     *  4/25 * 14/20
   + 4     *  4/25 *  6/20 * 28/30
   + 5     *  4/25 *  6/20 *  2/30 * 7/10
   + 6     *  4/25 *  6/20 *  2/30 * 3/10 * 14/15
   + (6+x) *  4/25 *  6/20 *  2/30 * 3/10 *  1/15
x = about 2.21 calls to rand5()
27 голосов
/ 15 ноября 2010

Алгоритм:

7 может быть представлено в последовательности из 3 битов

Используйте rand (5) для случайного заполнения каждого бита 0 или 1.
Например: позвоните rand (5) и

если результат равен 1 или 2, заполните бит 0
если результат равен 4 или 5, заполните бит 1
если результат равен 3, игнорируйте и сделайте это снова (отклонение)

Таким образом, мы можем произвольно заполнить 3 бита 0/1 и, таким образом, получить число от 1-7.

РЕДАКТИРОВАТЬ: Это кажется самым простым и эффективным ответом, поэтому вот код для него:

public static int random_7() {
    int returnValue = 0;
    while (returnValue == 0) {
        for (int i = 1; i <= 3; i++) {
            returnValue = (returnValue << 1) + random_5_output_2();
        }
    }
    return returnValue;
}

private static int random_5_output_2() {
    while (true) {
        int flip = random_5();

        if (flip < 3) {
            return 0;
        }
        else if (flip > 3) {
            return 1;
        }
    }
}
19 голосов
/ 26 сентября 2008
int randbit( void )
{
    while( 1 )
    {
        int r = rand5();
        if( r <= 4 ) return(r & 1);
    }
}

int randint( int nbits )
{
    int result = 0;
    while( nbits-- )
    {
        result = (result<<1) | randbit();
    }
    return( result );
}

int rand7( void )
{
    while( 1 )
    {
        int r = randint( 3 ) + 1;
        if( r <= 7 ) return( r );
    }
}
16 голосов
/ 26 сентября 2008
rand7() = (rand5()+rand5()+rand5()+rand5()+rand5()+rand5()+rand5())%7+1

Редактировать: Это не совсем работает. Это примерно на 2 части из 1000 (при условии идеального ранда5). Ведра получают:

value   Count  Error%
1       11158  -0.0035
2       11144  -0.0214
3       11144  -0.0214
4       11158  -0.0035
5       11172  +0.0144
6       11177  +0.0208
7       11172  +0.0144

путем переключения на сумму

n   Error%
10  +/- 1e-3,
12  +/- 1e-4,
14  +/- 1e-5,
16  +/- 1e-6,
...
28  +/- 3e-11

, кажется, получает порядок величины за каждые 2 добавленных

Кстати: приведенная выше таблица ошибок была сгенерирована не с помощью выборки, а с помощью следующего отношения повторения:

p[x,n] - это число способов, которыми output=x может произойти при n вызовах rand5.

  p[1,1] ... p[5,1] = 1
  p[6,1] ... p[7,1] = 0

  p[1,n] = p[7,n-1] + p[6,n-1] + p[5,n-1] + p[4,n-1] + p[3,n-1]
  p[2,n] = p[1,n-1] + p[7,n-1] + p[6,n-1] + p[5,n-1] + p[4,n-1]
  p[3,n] = p[2,n-1] + p[1,n-1] + p[7,n-1] + p[6,n-1] + p[5,n-1]
  p[4,n] = p[3,n-1] + p[2,n-1] + p[1,n-1] + p[7,n-1] + p[6,n-1]
  p[5,n] = p[4,n-1] + p[3,n-1] + p[2,n-1] + p[1,n-1] + p[7,n-1]
  p[6,n] = p[5,n-1] + p[4,n-1] + p[3,n-1] + p[2,n-1] + p[1,n-1]
  p[7,n] = p[6,n-1] + p[5,n-1] + p[4,n-1] + p[3,n-1] + p[2,n-1]
15 голосов
/ 26 сентября 2008
int ans = 0;
while (ans == 0) 
{
     for (int i=0; i<3; i++) 
     {
          while ((r = rand5()) == 3){};
          ans += (r < 3) >> i
     }
}
13 голосов
/ 15 января 2009

Следующее дает равномерное распределение на {1, 2, 3, 4, 5, 6, 7} с использованием генератора случайных чисел, создающего равномерное распределение на {1, 2, 3, 4, 5}. Код грязный, но логика ясна.

public static int random_7(Random rg) {
    int returnValue = 0;
    while (returnValue == 0) {
        for (int i = 1; i <= 3; i++) {
            returnValue = (returnValue << 1) + SimulateFairCoin(rg);
        }
    }
    return returnValue;
}

private static int SimulateFairCoin(Random rg) {
    while (true) {
        int flipOne = random_5_mod_2(rg);
        int flipTwo = random_5_mod_2(rg);

        if (flipOne == 0 && flipTwo == 1) {
            return 0;
        }
        else if (flipOne == 1 && flipTwo == 0) {
            return 1;
        }
    }
}

private static int random_5_mod_2(Random rg) {
    return random_5(rg) % 2;
}

private static int random_5(Random rg) {
    return rg.Next(5) + 1;
}    
12 голосов
/ 09 мая 2009

Если мы рассмотрим дополнительное ограничение попытки дать наиболее эффективный ответ, т.е. тот, который задал входной поток, I, из равномерно распределенных целых чисел длины m из 1-5, выводится поток O из равномерно распределенные целые числа от 1 до 7 самой длинной длины относительно m, скажем L(m).

Простейший способ проанализировать это - обработать потоки I и O как 5-значные и 7-значные числа соответственно. Это достигается с помощью идеи основного ответа о взятии потока a1, a2, a3,... -> a1+5*a2+5^2*a3+.. и аналогично для потока O.

Тогда, если мы возьмем секцию входного потока длиной m choose n s.t. 5^m-7^n=c, где c>0 и будет как можно меньше. Затем существует единообразная карта из входного потока длины m в целые числа от 1 до 5^m и еще одна унифицированная карта из целых чисел от 1 до 7^n в выходной поток длины n, где мы можем потерять несколько случаи из входного потока, когда отображаемое целое число превышает 7^n.

Таким образом, это дает значение для L(m) около m (log5/log7), что приблизительно равно .82m.

Сложность описанного выше анализа заключается в уравнении 5^m-7^n=c, которое нелегко точно решить, и в случае, когда равномерное значение от 1 до 5^m превышает 7^n, и мы теряем эффективность.

Вопрос в том, насколько близко может быть достигнуто наилучшее возможное значение m (log5 / log7). Например, когда это число приближается к целому числу, можем ли мы найти способ достижения этого точного целого числа выходных значений?

Если 5^m-7^n=c, то из входного потока мы эффективно генерируем равномерное случайное число от 0 до (5^m)-1 и не используем никаких значений выше 7^n. Однако эти значения могут быть восстановлены и использованы снова. Они эффективно генерируют единую последовательность чисел от 1 до 5^m-7^n. Таким образом, мы можем затем попытаться использовать их и преобразовать их в 7-разрядные числа, чтобы мы могли создать больше выходных значений.

Если мы примем T7(X) как среднюю длину выходной последовательности random(1-7) целых чисел, полученных из унифицированного ввода размера X, и предположим, что 5^m=7^n0+7^n1+7^n2+...+7^nr+s, s<7.

Тогда T7(5^m)=n0x7^n0/5^m + ((5^m-7^n0)/5^m) T7(5^m-7^n0), поскольку у нас нет длины без последовательности с вероятностью 7 ^ n0 / 5 ^ m с остатком длины 5^m-7^n0 с вероятностью (5^m-7^n0)/5^m).

Если мы просто продолжим замену, мы получим:

T7(5^m) = n0x7^n0/5^m + n1x7^n1/5^m + ... + nrx7^nr/5^m  = (n0x7^n0 + n1x7^n1 + ... + nrx7^nr)/5^m

Следовательно

L(m)=T7(5^m)=(n0x7^n0 + n1x7^n1 + ... + nrx7^nr)/(7^n0+7^n1+7^n2+...+7^nr+s)

Другой способ выразить это:

If 5^m has 7-ary representation `a0+a1*7 + a2*7^2 + a3*7^3+...+ar*7^r
Then L(m) = (a1*7 + 2a2*7^2 + 3a3*7^3+...+rar*7^r)/(a0+a1*7 + a2*7^2 + a3*7^3+...+ar*7^r)

Наилучший возможный случай - мой оригинальный выше, где 5^m=7^n+s, где s<7.

Затем T7(5^m) = nx(7^n)/(7^n+s) = n+o(1) = m (Log5/Log7)+o(1), как и раньше.

Худший случай, когда мы можем найти только k и s.t 5 ^ m = kx7 + s.

Then T7(5^m) = 1x(k.7)/(k.7+s) = 1+o(1)

Другие случаи находятся где-то посередине. Было бы интересно посмотреть, насколько хорошо мы можем сделать для очень больших m, то есть насколько хорошо мы можем получить ошибку:

T7(5^m) = m (Log5/Log7)+e(m)

Кажется невозможным достичь e(m) = o(1) в целом, но, надеюсь, мы сможем доказать e(m)=o(m).

В этом случае все зависит от распределения семизначных цифр 5^m для различных значений m.

Я уверен, что есть много теорий, которые охватывают это, я могу взглянуть и доложить в какой-то момент.

...