Почему люди говорят, что при использовании генератора случайных чисел существует смещение по модулю?

Я видел, как этот вопрос задавали много, но никогда не видел настоящего конкретного ответа на него. Итак, я собираюсь опубликовать здесь один, который, надеюсь, поможет людям понять, почему именно существует "смещение по модулю" при использовании генератора случайных чисел, например rand() в C ++.


person user1413793    schedule 11.06.2012    source источник


Ответы (10)


Итак, rand() - это генератор псевдослучайных чисел, который выбирает натуральное число от 0 до RAND_MAX, которое является константой, определенной в cstdlib (см. Это статья для общего обзора rand()).

Что произойдет, если вы захотите сгенерировать случайное число, скажем, от 0 до 2? Для объяснения предположим, что RAND_MAX равно 10, и я решил сгенерировать случайное число от 0 до 2, вызвав rand()%3. Однако rand()%3 не производит числа от 0 до 2 с равной вероятностью!

Когда rand() возвращает 0, 3, 6 или 9, rand()%3 == 0. Следовательно, P (0) = 4/11

Когда rand() возвращает 1, 4, 7 или 10, rand()%3 == 1. Следовательно, P (1) = 4/11

Когда rand() возвращает 2, 5 или 8, rand()%3 == 2. Следовательно, P (2) = 3/11.

Это не генерирует числа от 0 до 2 с равной вероятностью. Конечно, для небольших диапазонов это может быть не самой большой проблемой, но для большего диапазона это может исказить распределение, смещая меньшие числа.

Итак, когда rand()%n с равной вероятностью возвращает диапазон чисел от 0 до n-1? Когда RAND_MAX%n == n - 1. В этом случае, наряду с нашим предыдущим предположением, rand() действительно возвращает число от 0 до RAND_MAX с равной вероятностью, классы по модулю n также будут равномерно распределены.

Так как же решить эту проблему? Грубый способ - продолжать генерировать случайные числа, пока вы не получите число в желаемом диапазоне:

int x; 
do {
    x = rand();
} while (x >= n);

но это неэффективно для низких значений n, поскольку у вас есть только n/RAND_MAX шанс получить значение в вашем диапазоне, и поэтому вам нужно будет выполнять RAND_MAX/n вызов rand() в среднем.

Более эффективный подход к формуле заключался бы в том, чтобы взять некоторый большой диапазон с длиной, кратной n, например RAND_MAX - RAND_MAX % n, продолжать генерировать случайные числа, пока вы не получите число, лежащее в этом диапазоне, а затем возьмите модуль:

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;

Для небольших значений n это редко требует более одного вызова rand().


Процитированные и дополнительные материалы для чтения:


person user1413793    schedule 11.06.2012
comment
Другой способ думать о _RAND_MAX%n == n - 1_ - это (RAND_MAX + 1) % n == 0. Читая код, я склонен понимать % something == 0 как «делимый без остатка» с большей готовностью, чем другие способы его вычисления. Конечно, если ваш C ++ stdlib имеет RAND_MAX то же значение, что и INT_MAX, (RAND_MAX + 1) наверняка не будет работать; поэтому расчет Марка остается самой безопасной реализацией. - person Slipp D. Thompson; 19.07.2016
comment
Я могу придириться, но если цель состоит в том, чтобы уменьшить потери битов, мы могли бы немного улучшить это для граничного условия, когда RAND_MAX (RM) всего на 1 меньше, чем равное деление на N. В этом сценарии нет необходимости тратить биты на выполнение X ›= (RM - RM% N)), которое имеет небольшое значение для малых значений N, но становится более значительным для больших значений N. Как упоминал Слипп Д. Томпсон, существует решение, которое будет работать только когда INT_MAX (IM) ›RAND_MAX, но прерывается, когда они равны. Однако есть простое решение: мы можем изменить расчет X ›= (RM - RM% N) следующим образом: - person Ben Personick; 28.10.2017
comment
Я опубликовал дополнительный ответ, в котором подробно объясняется проблема и приводится пример решения кода. - person Ben Personick; 31.10.2017
comment
Предоставляет ли использование петли возможность для атаки по побочным каналам в этом случае? - person Rodolfo Carvalho; 26.11.2020

Постоянный выбор случайного числа - хороший способ устранить предвзятость.

Обновить

Мы могли бы сделать код быстрым, если бы искать x в диапазоне, кратном n.

// Assumptions
// rand() in [0, RAND_MAX]
// n in (0, RAND_MAX]

int x; 

// Keep searching for an x in a range divisible by n 
do {
    x = rand();
} while (x >= RAND_MAX - (RAND_MAX % n)) 

x %= n;

Вышеупомянутый цикл должен быть очень быстрым, скажем, в среднем 1 итерация.

person Nick Dandoulakis    schedule 12.06.2012
comment
Уф :-P преобразование в двойное, а затем умножение на MAX_UPPER_LIMIT / RAND_MAX намного чище и работает лучше. - person boycy; 13.06.2012
comment
@boycy: вы упустили суть. Если количество значений, которые rand() может вернуть, не кратно n, то, что бы вы ни делали, вы неизбежно получите «смещение по модулю», если вы не отбросите некоторые из этих значений. user1413793 прекрасно это объясняет (хотя решение, предложенное в этом ответе, действительно неприятное). - person TonyK; 17.06.2012
comment
@TonyK, приношу свои извинения, я упустил суть. Не подумал достаточно хорошо и подумал, что смещение будет применяться только к методам, использующим явную операцию модуля. Спасибо, что исправили меня :-) - person boycy; 18.06.2012
comment
Приоритет оператора обеспечивает правильную работу RAND_MAX+1 - (RAND_MAX+1) % n, но я все же думаю, что для ясности его следует записать как RAND_MAX+1 - ((RAND_MAX+1) % n). - person Linus Arver; 13.10.2012
comment
Это не сработает, если RAND_MAX == INT_MAX (как в большинстве систем). См. Мой второй комментарий к @ user1413793 выше. - person BlueRaja - Danny Pflughoeft; 07.11.2012
comment
В среднем случае этой функции всегда будет меньше двух итераций, независимо от того, какие RAND_MAX и n выбраны. - person Jared Nielsen; 03.07.2013
comment
Вы можете исправить жалобу BlueRaja, используя UPPER_LIMIT = RAND_MAX - (RAND_MAX % n). В некоторых случаях лишние n числа будут отклоняться без надобности, но это позволяет избежать переполнения. - person Ben Voigt; 18.11.2013
comment
@BenVoigt, но тогда, когда n == RAND_MAX, мы никогда не получим n как случайное значение. Смотрите мое обновление. - person Nick Dandoulakis; 19.11.2013
comment
Использование x > RAND_MAX + (-RAND_MAX-1)%n в качестве границы устранит небольшую неэффективность и правильно обработает случай, когда RAND_MAX равно INT_MAX. Но я согласен, что это выглядит не очень интуитивно. - person fishinear; 21.03.2016
comment
@ BlueRaja-DannyPflughoeft В большинстве систем? Я никогда не видел реализации libc, где RAND_MAX не 32767 - Visual libc от Microsoft, GLibC, BSD libc, даже в разных архитектурах - person cat; 26.06.2017

@ user1413793 правильно описывает проблему. Я не собираюсь обсуждать это дальше, за исключением одного замечания: да, для малых значений n и больших значений RAND_MAX смещение по модулю может быть очень маленьким. Но использование шаблона, вызывающего смещение, означает, что вы должны учитывать смещение каждый раз, когда вычисляете случайное число и выбираете разные шаблоны для разных случаев. И если вы сделаете неправильный выбор, ошибки, которые он вносит, будут незаметными и практически невозможно протестировать. По сравнению с простым использованием подходящего инструмента (например, arc4random_uniform), это дополнительная работа, а не меньшая работа. Выполнять больше работы и получать худшее решение - это ужасная инженерия, особенно когда делать все правильно каждый раз легко на большинстве платформ.

К сожалению, все реализации решения неверны или менее эффективны, чем должны быть. (У каждого решения есть различные комментарии, объясняющие проблемы, но ни одно из решений не было исправлено для их устранения.) Это может сбить с толку случайного ищущего ответа, поэтому я предлагаю здесь заведомо хорошую реализацию.

Опять же, лучшее решение - просто использовать arc4random_uniform на платформы, которые его предоставляют, или аналогичное решение для вашей платформы (например, _ 5_ на Java). Он будет делать правильные вещи без каких-либо затрат на код. Это почти всегда правильный звонок.

Если у вас нет arc4random_uniform, вы можете использовать возможности открытого исходного кода, чтобы точно увидеть, как он реализован поверх более широкого диапазона ГСЧ (в данном случае ar4random, но аналогичный подход может также работать поверх других ГСЧ) .

Вот реализация OpenBSD:

/*
 * Calculate a uniformly distributed random number less than upper_bound
 * avoiding "modulo bias".
 *
 * Uniformity is achieved by generating new random numbers until the one
 * returned is outside the range [0, 2**32 % upper_bound).  This
 * guarantees the selected random number will be inside
 * [2**32 % upper_bound, 2**32) which maps back to [0, upper_bound)
 * after reduction modulo upper_bound.
 */
u_int32_t
arc4random_uniform(u_int32_t upper_bound)
{
    u_int32_t r, min;

    if (upper_bound < 2)
        return 0;

    /* 2**32 % x == (2**32 - x) % x */
    min = -upper_bound % upper_bound;

    /*
     * This could theoretically loop forever but each retry has
     * p > 0.5 (worst case, usually far better) of selecting a
     * number inside the range we need, so it should rarely need
     * to re-roll.
     */
    for (;;) {
        r = arc4random();
        if (r >= min)
            break;
    }

    return r % upper_bound;
}

Стоит отметить последний комментарий коммита по этому коду для тех, кому нужно реализовать подобные вещи:

Измените arc4random_uniform () для вычисления 2**32 % upper_bound как -upper_bound % upper_bound. Упрощает код и делает его одинаковым для архитектур ILP32 и LP64, а также немного быстрее на архитектурах LP64 за счет использования 32-битного остатка вместо 64-битного остатка.

На что указал Джорден Вервер на tech @ ok deraadt; никаких возражений со стороны djm или otto

Реализацию Java также легко найти (см. Предыдущую ссылку):

public int nextInt(int n) {
   if (n <= 0)
     throw new IllegalArgumentException("n must be positive");

   if ((n & -n) == n)  // i.e., n is a power of 2
     return (int)((n * (long)next(31)) >> 31);

   int bits, val;
   do {
       bits = next(31);
       val = bits % n;
   } while (bits - val + (n-1) < 0);
   return val;
 }
person Rob Napier    schedule 18.11.2013
comment
Обратите внимание, что если arcfour_random() действительно использует реальный алгоритм RC4 в своей реализации, выходные данные определенно будут иметь некоторую предвзятость. Надеюсь, авторы вашей библиотеки перешли на использование более качественного CSPRNG за тем же интерфейсом. Я помню, что одна из BSD теперь фактически использует алгоритм ChaCha20 для реализации arcfour_random(). Подробнее о предвзятости вывода RC4, которая делает его бесполезным для безопасности или других критически важных приложений, таких как видеопокер: blog.cryptographyengineering.com/2013/03/ - person rmalayter; 09.08.2016
comment
@rmalayter В iOS и OS X arc4random читает из / dev / random, что является самым высоким качеством энтропии в системе. (Арка 4 в названии является исторической и сохранена для совместимости.) - person Rob Napier; 09.08.2016
comment
@Rob_Napier полезно знать, но /dev/random также использовал RC4 на некоторых платформах в прошлом (Linux использует SHA-1 в режиме счетчика). К сожалению, страницы руководства, которые я нашел через поиск, показывают, что RC4 все еще используется на различных платформах, предлагающих arc4random (хотя фактический код может быть другим). - person rmalayter; 09.08.2016
comment
Я запутался. Разве не -upper_bound % upper_bound == 0 ?? - person Jon McClung; 09.03.2019
comment
@JonMcClung Это очень хороший вопрос, но ответ (на удивление) нет. Это uint32_t, поэтому, если x больше 2 ^ 31, -x фактически положительно (поскольку в этом контексте оно оценивается как целое число со знаком). О, слава беззнаковому ... Например, -2147483650, оцененное как UInt32, равно 2147483646, а -4294967290 равно 6. - person Rob Napier; 09.03.2019
comment
@RobNapier ага! Спасибо за объяснение. Но гарантируется ли двухкомпонентное представление? Даже если это для Objective-C, этот вопрос помечен как C ++ / language-agnostic. - person Jon McClung; 09.03.2019
comment
Этот код - C, и он гарантированно работает на C. В расширении он также применим к Objective-C и C ++. Это не сработает на языке, который не обрабатывает целые числа без знака так же, как C, например на Swift. (Я не думаю, что у Go такое же неподписанное поведение, но я не могу вспомнить.) - person Rob Napier; 09.03.2019
comment
@JonMcClung -upper_bound % upper_bound действительно будет 0, если int шире 32 бит. Он должен быть (u_int32_t)-upper_bound % upper_bound) (при условии, что u_int32_t - это BSD-изм для uint32_t). - person Ian Abbott; 15.08.2019
comment
Не обращайте внимания на фальшивый ) в моем сообщении выше! :) - person Ian Abbott; 15.08.2019

Определение

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

Этого смещения особенно трудно избежать в вычислениях, где числа представлены в виде цепочек битов: нулей и единиц. Найти действительно случайные источники случайности также чрезвычайно сложно, но это выходит за рамки этого обсуждения. В оставшейся части этого ответа предположим, что существует неограниченный источник действительно случайных битов.

Пример проблемы

Давайте рассмотрим моделирование броска кубика (от 0 до 5) с использованием этих случайных битов. Есть 6 возможностей, поэтому нам нужно достаточно битов для представления числа 6, что составляет 3 бита. К сожалению, 3 случайных бита дают 8 возможных результатов:

000 = 0, 001 = 1, 010 = 2, 011 = 3
100 = 4, 101 = 5, 110 = 6, 111 = 7

Мы можем уменьшить размер набора результатов ровно до 6, взяв значение по модулю 6, однако это представляет проблему смещения по модулю: 110 дает 0, а 111 дает 1. Этот кубик загружен.

Возможные решения

Подход 0:

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

Подход 1:

Вместо использования модуля наивное, но математически правильное решение - отбросить результаты, которые дают 110 и 111, и просто повторить попытку с 3 новыми битами. К сожалению, это означает, что при каждом броске есть 25% шанс, что потребуется повторный бросок, включая каждый повторный броск. Это явно непрактично для всех случаев, кроме самых тривиальных.

Подход 2:

Используйте больше битов: вместо 3 бит используйте 4. Это дает 16 возможных результатов. Конечно, повторная прокрутка в любое время, когда результат больше 5, ухудшает ситуацию (10/16 = 62,5%), так что это само по себе не поможет.

Обратите внимание, что 2 * 6 = 12 ‹16, поэтому мы можем безопасно взять любой результат меньше 12 и уменьшить его по модулю 6, чтобы равномерно распределить результаты. Остальные 4 исхода должны быть отброшены, а затем повторно выброшены, как и в предыдущем подходе.

Сначала звучит неплохо, но давайте проверим математику:

4 discarded results / 16 possibilities = 25%

В этом случае 1 дополнительный бит не помог!

Результат досадный, но давайте попробуем еще раз с 5 битами:

32 % 6 = 2 discarded results; and
2 discarded results / 32 possibilities = 6.25%

Несомненное улучшение, но во многих практических случаях недостаточно хорошее. Хорошая новость заключается в том, что добавление дополнительных битов никогда не увеличит шансы на необходимость сброса и повторного броска. Это справедливо не только для игральных костей, но и во всех случаях.

Как было продемонстрировано, однако добавление 1 дополнительного бита может ничего не изменить. Фактически, если мы увеличим наш результат до 6 бит, вероятность останется 6,25%.

Это вызывает 2 дополнительных вопроса:

  1. Если мы добавим достаточно битов, есть ли гарантия, что вероятность сброса уменьшится?
  2. Сколько бит достаточно в общем случае?

Общее решение

К счастью, ответ на первый вопрос - да. Проблема с 6 заключается в том, что 2 ^ x mod 6 переворачивается между 2 и 4, которые по совпадению кратны 2 друг от друга, так что для четного x> 1,

[2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1)

Таким образом, 6 - скорее исключение, чем правило. Можно найти более крупные модули, которые дают последовательные степени 2 таким же образом, но в конечном итоге это должно быть циклически повторяться, и вероятность отбрасывания будет уменьшена.

Без дополнительных доказательств, как правило, использование удвоенного количества требуемых битов обеспечивает меньшую, обычно незначительную, вероятность отбрасывания.

Доказательство концепции

Вот пример программы, которая использует OpenSSL libcrypo для предоставления случайных байтов. При компиляции обязательно подключайтесь к библиотеке с -lcrypto, которая должна быть доступна почти каждому.

#include <iostream>
#include <assert.h>
#include <limits>
#include <openssl/rand.h>

volatile uint32_t dummy;
uint64_t discardCount;

uint32_t uniformRandomUint32(uint32_t upperBound)
{
    assert(RAND_status() == 1);
    uint64_t discard = (std::numeric_limits<uint64_t>::max() - upperBound) % upperBound;
    uint64_t randomPool = RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));

    while(randomPool > (std::numeric_limits<uint64_t>::max() - discard)) {
        RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));
        ++discardCount;
    }

    return randomPool % upperBound;
}

int main() {
    discardCount = 0;

    const uint32_t MODULUS = (1ul << 31)-1;
    const uint32_t ROLLS = 10000000;

    for(uint32_t i = 0; i < ROLLS; ++i) {
        dummy = uniformRandomUint32(MODULUS);
    }
    std::cout << "Discard count = " << discardCount << std::endl;
}

Я рекомендую поиграть со значениями MODULUS и ROLLS, чтобы увидеть, сколько повторов на самом деле происходит в большинстве условий. Скептик может также пожелать сохранить вычисленные значения в файл и убедиться, что распределение выглядит нормальным.

person Jim Wood    schedule 23.04.2015
comment
Я очень надеюсь, что никто не скопировал слепо вашу равномерную случайную реализацию. Строка randomPool = RAND_bytes(...) всегда будет приводить к randomPool == 1 из-за утверждения. Это всегда приводит к сбросу и повторному броску. Думаю, вы хотели объявить отдельной строкой. Следовательно, это заставляло ГСЧ возвращать 1 для каждой итерации. - person Qix - MONICA WAS MISTREATED; 22.12.2017
comment
Для ясности: randomPool всегда будет оценивать как 1 в соответствии с документацией OpenSSL для RAND_bytes(), поскольку благодаря утверждению RAND_status() он всегда будет успешным. - person Qix - MONICA WAS MISTREATED; 22.12.2017

Решение Марка (принятое решение) почти идеально.

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;

Создан 25 мар.

Марк Эмери 39k21170211

Однако у него есть предостережение, которое отбрасывает 1 действительный набор результатов в любом сценарии, где RAND_MAX (RM) на 1 меньше, чем кратное N (где N = количество возможных действительных результатов).

то есть, когда «количество отброшенных значений» (D) равно N, тогда они фактически являются допустимым набором (V), а не недопустимым набором (I).

Причина этого в том, что в какой-то момент Марк упускает из виду разницу между N и Rand_Max.

N - это набор, действительные члены которого состоят только из положительных целых чисел, так как он содержит количество ответов, которые будут действительными. (например: Установить N = {1, 2, 3, ... n })

Rand_max Однако это набор, который (как определено для наших целей) включает любое количество неотрицательных целых чисел.

В наиболее общей форме то, что здесь определяется как Rand Max, представляет собой Набор всех допустимых результатов, которые теоретически могут включать отрицательные числа или нечисловые значения.

Поэтому Rand_Max лучше определить как набор возможных ответов.

Однако N работает с подсчетом значений в наборе действительных ответов, поэтому, даже как определено в нашем конкретном случае, Rand_Max будет значением, на единицу меньшим, чем общее число, которое оно содержит.

Используя решение Марка, значения отбрасываются, когда: X = ›RM - RM% N

EG: 

Ran Max Value (RM) = 255
Valid Outcome (N) = 4

When X => 252, Discarded values for X are: 252, 253, 254, 255

So, if Random Value Selected (X) = {252, 253, 254, 255}

Number of discarded Values (I) = RM % N + 1 == N

 IE:

 I = RM % N + 1
 I = 255 % 4 + 1
 I = 3 + 1
 I = 4

   X => ( RM - RM % N )
 255 => (255 - 255 % 4) 
 255 => (255 - 3)
 255 => (252)

 Discard Returns $True

Как вы можете видеть в приведенном выше примере, когда значение X (случайное число, которое мы получаем из начальной функции) равно 252, 253, 254 или 255, мы отбрасываем его, даже если эти четыре значения составляют действительный набор возвращаемых значений. .

IE: когда количество отклоненных значений (I) = N (количество допустимых результатов), то исходная функция отбрасывает верный набор возвращаемых значений.

Если мы опишем разницу между значениями N и RM как D, то есть:

D = (RM - N)

Затем, когда значение D становится меньше, процент ненужных повторных бросков из-за этого метода увеличивается при каждом натуральном мультипликативе. (Когда RAND_MAX НЕ равно простому числу, это вызывает серьезную озабоченность)

EG:

RM=255 , N=2 Then: D = 253, Lost percentage = 0.78125%

RM=255 , N=4 Then: D = 251, Lost percentage = 1.5625%
RM=255 , N=8 Then: D = 247, Lost percentage = 3.125%
RM=255 , N=16 Then: D = 239, Lost percentage = 6.25%
RM=255 , N=32 Then: D = 223, Lost percentage = 12.5%
RM=255 , N=64 Then: D = 191, Lost percentage = 25%
RM=255 , N= 128 Then D = 127, Lost percentage = 50%

Поскольку процент необходимых повторных операций увеличивается по мере приближения N к RM, это может вызывать серьезную озабоченность при многих различных значениях в зависимости от ограничений системы, в которой выполняется код, и значений, которые ищутся.

Чтобы опровергнуть это, мы можем внести простую поправку, как показано здесь:

 int x;
 
 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );
 
 x %= n;

Это обеспечивает более общую версию формулы, которая учитывает дополнительные особенности использования модуля для определения ваших максимальных значений.

Примеры использования небольшого значения для RAND_MAX, которое является мультипликативом N.

Отметить оригинальную версию:

RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X >= (RAND_MAX - ( RAND_MAX % n ) )
When X >= 2 the value will be discarded, even though the set is valid.

Обобщенная версия 1:

RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X > (RAND_MAX - ( ( RAND_MAX % n  ) + 1 ) % n )
When X > 3 the value would be discarded, but this is not a vlue in the set RAND_MAX so there will be no discard.

Кроме того, в случае, когда N должно быть количеством значений в RAND_MAX; в этом случае вы можете установить N = RAND_MAX +1, если RAND_MAX = INT_MAX.

По циклу вы можете просто использовать N = 1, и любое значение X будет принято, однако, и вставьте оператор IF для вашего окончательного множителя. Но, возможно, у вас есть код, который может иметь вескую причину для возврата 1, когда функция вызывается с n = 1 ...

Поэтому может быть лучше использовать 0, который обычно дает ошибку Div 0, если вы хотите, чтобы n = RAND_MAX + 1

Обобщенная версия 2:

int x;

if n != 0 {
    do {
        x = rand();
    } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

    x %= n;
} else {
    x = rand();
}

Оба эти решения решают проблему с ненужным отбрасыванием действительных результатов, которые возникают, когда RM + 1 является произведением n.

Вторая версия также охватывает сценарий крайнего случая, когда вам нужно, чтобы n равнялось общему возможному набору значений, содержащемуся в RAND_MAX.

Модифицированный подход в обоих случаях одинаков и позволяет найти более общее решение потребности в предоставлении действительных случайных чисел и минимизации отброшенных значений.

Повторить:

Базовое общее решение, расширяющее пример знака:

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

 int x;
 
 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );
 
 x %= n;

Расширенное общее решение, допускающее еще один сценарий RAND_MAX + 1 = n:

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x;

if n != 0 {
    do {
        x = rand();
    } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );

    x %= n;
} else {
    x = rand();
}

В некоторых языках (особенно в интерпретируемых языках) выполнение вычислений операции сравнения вне условия while может привести к более быстрым результатам, поскольку это однократное вычисление, независимо от того, сколько повторных попыток требуется. YMMV!

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x; // Resulting random number
int y; // One-time calculation of the compare value for x

y = RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) 

if n != 0 {
    do {
        x = rand();
    } while (x > y);

    x %= n;
} else {
    x = rand();
}
person Ben Personick    schedule 28.10.2017
comment
Разве не безопасно сказать, что проблема с решением Марка состоит в том, что он рассматривает RAND_MAX и n как одну и ту же единицу измерения, хотя на самом деле они означают две разные вещи? В то время как n представляет результирующее количество возможностей, RAND_MAX представляет только максимальное значение исходной возможности, где RAND_MAX + 1 будет исходным количеством возможностей. Я удивлен, что он не пришел к вашему заключению, поскольку он, похоже, признал, что n и RAND_MAX - это не одно и то же с уравнением: RAND_MAX%n = n - 1 - person Danilo Souza Morães; 11.08.2019
comment
@ DaniloSouzaMorães Спасибо, Данило, Вы очень лаконично изложили этот вопрос. Я пошел на демонстрацию того, что он делал вместе с объяснением почему и как, но не думаю, что когда-либо смог красноречиво заявить, ЧТО он делал неправильно, так как я настолько погружен в детали логики того, как и почему существует проблема, что я не так четко излагаю, о чем идет речь. Вы не возражаете, если я изменю свой ответ, чтобы использовать часть того, что вы здесь написали, как свое собственное резюме по вопросу о том, что и где принимает принятое решение, что необходимо решить в верхней части? - person Ben Personick; 16.10.2019
comment
Это было бы круто. Действуй - person Danilo Souza Morães; 16.10.2019

Есть две обычные жалобы на использование модуля по модулю.

  • один действителен для всех генераторов. Это легче увидеть в предельном случае. Если ваш генератор имеет RAND_MAX, равный 2 (что не соответствует стандарту C), и вы хотите только 0 или 1 в качестве значения, использование modulo будет генерировать 0 в два раза чаще (когда генератор генерирует 0 и 2), чем он будет генерировать 1 (когда генератор генерирует 1). Обратите внимание, что это верно, как только вы не отбрасываете значения, независимо от того, какое отображение вы используете от значений генератора к желаемому, одно будет происходить в два раза чаще, чем другое.

  • У какого-то генератора менее значимые биты менее случайны, чем у другого, по крайней мере, для некоторых параметров, но, к сожалению, у этих параметров есть другая интересная характеристика (например, возможность иметь RAND_MAX на единицу меньше степени 2). Проблема хорошо известна, и в течение долгого времени реализация библиотеки, вероятно, избегала проблемы (например, реализация примера rand () в стандарте C использует этот тип генератора, но отбрасывает 16 менее значимых битов), но некоторые любят жаловаться на это и тебе может не повезти

Используя что-то вроде

int alea(int n){ 
 assert (0 < n && n <= RAND_MAX); 
 int partSize = 
      n == RAND_MAX ? 1 : 1 + (RAND_MAX-n)/(n+1); 
 int maxUsefull = partSize * n + (partSize-1); 
 int draw; 
 do { 
   draw = rand(); 
 } while (draw > maxUsefull); 
 return draw/partSize; 
}

для генерации случайного числа от 0 до n позволит избежать обеих проблем (и избежать переполнения с помощью RAND_MAX == INT_MAX)

Кстати, C ++ 11 представил стандартные способы сокращения и другие генераторы, кроме rand ().

person AProgrammer    schedule 13.06.2012
comment
n == RAND_MAX? 1: (RAND_MAX-1) / (n + 1): Я понимаю, что идея здесь состоит в том, чтобы сначала разделить RAND_MAX на равный размер страницы N, а затем вернуть отклонение в пределах N, но я не могу точно сопоставить код с этим. - person zinking; 15.06.2012
comment
Наивная версия должна быть (RAND_MAX + 1) / (n + 1), поскольку есть значения RAND_MAX + 1, которые нужно разделить на n + 1 сегменты. Если во избежание переполнения при вычислении RAND_MAX + 1, его можно преобразовать в 1+ (RAND_MAX-n) / (n + 1). Чтобы избежать переполнения при вычислении n + 1, сначала проверяется случай n == RAND_MAX. - person AProgrammer; 15.06.2012
comment
+ плюс, кажется, что деление обходится дороже по сравнению с регенерированными числами. - person zinking; 15.06.2012
comment
Взятие по модулю и деление имеют одинаковую стоимость. Некоторые ISA даже предоставляют только одну инструкцию, которая всегда обеспечивает обе. Стоимость восстановления чисел будет зависеть от n и RAND_MAX. Если n мало по отношению к RAND_MAX, это может дорого стоить. И, очевидно, вы можете решить, что предубеждения не важны для вашего приложения; Я просто даю возможность их избегать. - person AProgrammer; 15.06.2012

При значении RAND_MAX, равном 3 (на самом деле оно должно быть намного выше, но смещение все равно будет), из этих вычислений следует, что смещение имеет место:

1 % 2 = 1 2 % 2 = 0 3 % 2 = 1 random_between(1, 3) % 2 = more likely a 1

В этом случае % 2 - это то, что вам не следует делать, если вам нужно случайное число от 0 до 1. Вы можете получить случайное число от 0 до 2, выполнив % 3, потому что в этом случае: RAND_MAX кратно 3.

Другой способ

Есть намного проще, но чтобы добавить к другим ответам, вот мое решение для получения случайного числа между 0 и n - 1, поэтому n разные возможности, без предвзятости.

  • количество битов (не байтов), необходимых для кодирования количества возможностей - это количество битов случайных данных, которые вам понадобятся
  • закодировать число из случайных битов
  • если это число >= n, перезапустите (без модуля).

На самом деле случайные данные получить непросто, так зачем использовать больше бит, чем нужно.

Ниже приведен пример на Smalltalk, использующий кэш битов от генератора псевдослучайных чисел. Я не эксперт по безопасности, так что используйте на свой страх и риск.

next: n

    | bitSize r from to |
    n < 0 ifTrue: [^0 - (self next: 0 - n)].
    n = 0 ifTrue: [^nil].
    n = 1 ifTrue: [^0].
    cache isNil ifTrue: [cache := OrderedCollection new].
    cache size < (self randmax highBit) ifTrue: [
        Security.DSSRandom default next asByteArray do: [ :byte |
            (1 to: 8) do: [ :i |    cache add: (byte bitAt: i)]
        ]
    ].
    r := 0.
    bitSize := n highBit.
    to := cache size.
    from := to - bitSize + 1.
    (from to: to) do: [ :i |
        r := r bitAt: i - from + 1 put: (cache at: i)
    ].
    cache removeFrom: from to: to.
    r >= n ifTrue: [^self next: n].
    ^r
person Rivenfall    schedule 11.08.2016

Снижение по модулю - это распространенный способ заставить генератор случайных целых чисел избежать наихудшего случая бесконечной работы.

Однако, когда диапазон возможных целых чисел неизвестен, в целом нет способа исправить этот наихудший случай бесконечного выполнения без внесения смещения. Таким образом, не только уменьшение по модулю (rand() % n, обсуждалось в принятом ответе), но и уменьшение умножения и сдвига Даниэля Лемира, или если вы перестанете отклонять результат после заданного количества итераций. (Для ясности, это не означает, что нет способа исправить проблемы смещения, присутствующие в генераторах псевдослучайных случаев. Например, даже если по модулю и другие сокращения в целом смещены, у них не будет проблем со смещением, если диапазон возможных целые числа - это степень двойки и, если генератор случайных чисел производит несмещенные случайные биты или их блоки.)

Остальная часть этого ответа покажет взаимосвязь между временем работы и смещением в случайных генераторах. С этого момента мы будем предполагать, что у нас есть настоящий генератор случайных чисел, который может производить несмещенные и независимые случайные биты. *

В 1976 году DE Knuth и AC Yao показали, что любой алгоритм, который производит случайные целые числа с заданной вероятностью, используя только случайные биты, может быть представлен в виде двоичного дерева, где случайные биты указывают, каким путем пройти по дереву и каждому листу (конечной точке). соответствует исходу. В этом случае мы имеем дело с алгоритмами, которые генерируют случайные целые числа в [0, n), где каждое целое число выбирается с вероятностью 1 / n. Алгоритм является беспристрастным, если в дереве появляется одинаковое количество листьев для всех результатов. Но если 1 / n имеет неограниченное двоичное раскрытие (что будет иметь место, если n не является степенью 2), алгоритм будет несмещенным, только если:

  • двоичное дерево имеет бесконечную глубину, или
  • бинарное дерево включает в себя отклоняемые листья на конце,

и в любом случае алгоритм не будет работать в постоянное время, а в худшем случае будет работать вечно. (С другой стороны, когда n является степенью 2, оптимальное двоичное дерево будет иметь конечную глубину и не будет узлов отклонения.)

Концепция двоичного дерева также показывает, что любой способ исправить эту временную сложность наихудшего случая приведет к смещению в целом. (Опять же, это не означает, что нет способа исправить проблемы смещения, присутствующие в псевдослучайных генераторах.) Например, сокращения по модулю эквивалентны двоичному дереву, в котором отклоняемые листья заменяются помеченными результатами, но поскольку существует больше возможных результаты, чем отклонение, только некоторые из результатов могут занять место отклонения, привнося предвзятость. Тот же тип двоичного дерева - и такая же систематическая ошибка - дает результат, если вы перестанете отклонять после заданного количества итераций. (Однако это смещение может быть незначительным в зависимости от приложения. Существуют также аспекты безопасности при генерации случайных целых чисел, которые слишком сложно обсуждать в этом ответе.)

Для иллюстрации следующий код JavaScript реализует алгоритм случайных целых чисел, называемый Fast Dice Roller Дж. Ламброзо ( 2013). Обратите внимание, что он включает в себя событие отклонения и цикл, которые необходимы для обеспечения беспристрастности алгоритма в общем случае.

function randomInt(minInclusive, maxExclusive) {
 var maxInclusive = (maxExclusive - minInclusive) - 1
 var x = 1
 var y = 0
 while(true) {
    x = x * 2
    var randomBit = (Math.random() < 0.5 ? 0 : 1)
    y = y * 2 + randomBit
    if(x > maxInclusive) {
      if (y <= maxInclusive) { return y + minInclusive }
      // Rejection
      x = x - maxInclusive - 1
      y = y - maxInclusive - 1
    }
 }
}

Примечание

* Этот ответ не будет включать функцию rand() в C, потому что он есть много проблем. Возможно, наиболее серьезным здесь является тот факт, что стандарт C явно не определяет конкретное распределение для чисел, возвращаемых rand(), даже не однородное распределение.

person Peter O.    schedule 14.07.2020
comment
Помимо заботы о смещенном диапазоне, который не должен иметь никакого отношения к вопросу OP (какой IMP во всех ответах здесь, включая этот, кажется, только служит для того, чтобы замутить воду в отношении того, что выполняется). Тем не менее, этот код, похоже, просто обращается к той же основной причине самого модульного смещения, а именно: RAND_MAX всегда будет степенью 2, и поэтому, когда SET НЕ является степенью 2, вы должны отбросить значения, попадающие в плохой набор. Об этом говорится в моем и принятом ответе, но вы, кажется, думаете, что это не так ... - person Ben Personick; 06.01.2021
comment
@BenPersonick: В моем ответе говорится, что нет способа исправить наихудший случай вечной работы без введения предвзятости, а не о том, что нет способа исправить проблемы смещения, присутствующие с псевдослучайными генераторами. Когда диапазон целых чисел неизвестен, проблема смещения может быть решена, как правило, только с помощью выборки отклонения, такой как методы, указанные в вашем ответе или этом, а выборка отклонения имеет неограниченное время работы в наихудшем случае. Я уточню этот ответ. - person Peter O.; 07.01.2021
comment
А, я понял, мне было не до конца ясно, что вы хотели поднять неявную проблему, которую представляет весь наш код. Хотя, практически говоря, ваши шансы на то, что это будет работать вечно, весьма незначительны, если только базовая генерация псевдослучайных чисел не имеет значительной систематической ошибки. Каждый раунд имеет шанс сбросить карты и никогда не достигает 50%, - person Ben Personick; 07.01.2021
comment
Т.е. 2^(N-1)-1 - это максимальное отклонение (где N - степень двойки, которая представляет набор наших доходов RAND_MAX --- i3 2^N - это счетчик набора значений, которые может возвращать случайная функция, в то время как RAND_MAX равно 2^N-1) Таким образом, для простоты обзора мы будем называть максимальный шанс сброса 1/2 в каждом раунде. Может ли это продолжаться вечно? Да, это возможно, но разве нет? Это невероятно. - person Ben Personick; 07.01.2021
comment
@BenPersonick: Да, выборка отклонения может быть реализована в постоянное ожидаемое время, как вы упомянули. - person Peter O.; 07.01.2021
comment
Таким образом, для нашего аргумента, использующего 1/2 шанса (также известную как вероятность 50%), у нас есть (удивление) еще одна степень 2. Ваша вероятность иметь отброшенный набор X раз равна 1/(2^X) (например, 1 сброс 1/(2^1) 50% - 4 сброса равняется 1/16 6,25 % - 10 сбросов 1/1024 0,00098% - чтобы быть сброшенным 100 раз подряд, будет 1/(2^100), что составляет 7,88860905E-31%, на самом деле вы можете добавить проценты каждого броска, чтобы увидеть, что 93.75% of attemps would discard no more than 4 times и что 96.87% больше не сбрасываются. более 5 раз. 98.43 %в пределах 6 попыток, шанс 99,88% не более 10 сбросов. Это худший случай - person Ben Personick; 07.01.2021

Как показывает принятый ответ, «смещение по модулю» имеет свои корни в низком значении RAND_MAX. Он использует чрезвычайно маленькое значение RAND_MAX (10), чтобы показать, что если бы RAND_MAX было 10, то вы попытались сгенерировать число от 0 до 2 с помощью%, результатом были бы следующие результаты:

rand() % 3   // if RAND_MAX were only 10, gives
output of rand()   |   rand()%3
0                  |   0
1                  |   1
2                  |   2
3                  |   0
4                  |   1
5                  |   2
6                  |   0
7                  |   1
8                  |   2
9                  |   0

Итак, есть 4 выхода из 0 (шанс 4/10) и только 3 выхода из 1 и 2 (каждый из 3/10 шансов).

Так что это необъективно. У меньших чисел больше шансов выйти.

Но это проявляется так очевидно, только когда RAND_MAX маленький. Или, более конкретно, когда количество, которое вы модифицируете, велико по сравнению с RAND_MAX.

Намного лучшее решение, чем цикл (который безумно неэффективен, и его даже не следует предлагать), - это использовать ГПСЧ с гораздо большим выходным диапазоном. Алгоритм Mersenne Twister имеет максимальный выход 4 294 967 295. Таким образом, выполнение MersenneTwister::genrand_int32() % 10 для всех намерений и целей будет равномерно распределено, и эффект смещения по модулю практически исчезнет.

person bobobobo    schedule 15.04.2013
comment
Ваш более эффективен, и, вероятно, верно, что если RAND_MAX значительно больше, чем число, которое вы модифицируете, однако ваше значение все равно будет предвзятым. Конечно, это все генераторы псевдослучайных чисел в любом случае, и это само по себе является другой темой, но если вы предполагаете, что генератор полностью случайных чисел, ваш способ по-прежнему смещает более низкие значения. - person user1413793; 16.04.2013
comment
Поскольку наибольшее значение нечетное, MT::genrand_int32()%2 выбирает 0 (50 + 2,3e-8)% времени и 1 (50 - 2,3e-8)% времени. Если вы не создаете RGN казино (для которого вы, вероятно, использовали бы гораздо больший диапазон RGN), ни один пользователь не заметит лишних 2,3-8% времени. Вы говорите о числах, которые здесь слишком малы, чтобы иметь значение. - person bobobobo; 16.04.2013
comment
Зацикливание - лучшее решение. Это не безумно неэффективно; требуя менее чем в два раза больше итераций в худшем среднем случае. Использование высокого значения RAND_MAX уменьшит смещение по модулю, но не устранит его. Зацикливание будет. - person Jared Nielsen; 03.07.2013
comment
Если RAND_MAX значительно больше, чем число, которое вы модифицируете, количество раз, которое вам нужно регенерировать случайное число, исчезающе мало и не повлияет на эффективность. Я говорю, продолжайте цикл, пока вы тестируете против наибольшего кратного n, а не только n, как предлагается в принятом ответе. - person Mark Ransom; 08.04.2015

Я только что написал код для метода беспристрастного подбрасывания монет фон Неймана, который теоретически должен устранить любую систематическую ошибку в процессе генерации случайных чисел. Более подробную информацию можно найти на (http://en.wikipedia.org/wiki/Fair_coin)

int unbiased_random_bit() {    
    int x1, x2, prev;
    prev = 2;
    x1 = rand() % 2;
    x2 = rand() % 2;

    for (;; x1 = rand() % 2, x2 = rand() % 2)
    {
        if (x1 ^ x2)      // 01 -> 1, or 10 -> 0.
        {
            return x2;        
        }
        else if (x1 & x2)
        {
            if (!prev)    // 0011
                return 1;
            else
                prev = 1; // 1111 -> continue, bias unresolved
        }
        else
        {
            if (prev == 1)// 1100
                return 0;
            else          // 0000 -> continue, bias unresolved
                prev = 0;
        }
    }
}
person Yavuz Koroglu    schedule 09.04.2014
comment
Это не касается смещения по модулю. Этот процесс можно использовать для устранения смещения в потоке битов. Однако для перехода от потока битов к равномерному распределению от 0 до n, где n не на единицу меньше степени двойки, требуется адресация смещения по модулю. Таким образом, это решение не может устранить какой-либо систематической ошибки в процессе генерации случайных чисел. - person Rick; 05.08.2015
comment
@ Рик хм. Логическим расширением метода фон Неймана для устранения смещения по модулю при генерации случайного числа, скажем, от 1 до 100, было бы: A) вызвать rand() % 100 100 раз. Б) если все результаты разные, беру первый. C) в противном случае GOTO A. Это сработает, но с ожидаемым числом итераций около 10 ^ 42 вам придется набраться терпения. И бессмертный. - person Mark Amery; 27.03.2016
comment
@MarkAmery Действительно, это должно сработать. Просматривая этот алгоритм, хотя он неправильно реализован. Первый else должен быть: else if(prev==2) prev= x1; else { if(prev!=x1) return prev; prev=2;} - person Rick; 28.03.2016