Можно ли использовать `rand()` в С++ для создания объективных логических значений?

Я написал следующую функцию

bool random_bool(double probability)
{
    double p_scaled = probability * (RAND_MAX+1) - rand();
    if ( p_scaled >= 1 ) return true;
    if ( p_scaled <= 0 ) return false;
    return random_bool( p_scaled );
}

Учитывая, что rand() генерирует число из равномерного распределения по {0,1,...,RAND_MAX-1,RAND_MAX}, а числа из последующих вызовов могут рассматриваться как независимые для всех практических целей, кроме криптографии, это должно возвращать true с вероятностью p: два оператора if возвращают true с вероятностью немного ниже p, и false с вероятностью чуть выше 1-p, а рекурсивный вызов занимается всем остальным.

Однако следующий тест не проходит:

long long N = 10000000000; //1e10
double p = 10000.0 / N;
int counter = 0;
for (long long i=0;i<N;i++) if (random_bool(p)) counter++;
assert(9672 < counter && counter <= 10330);

Оператор assert рассчитан на отказ только в 0,1% случаев. Однако он все время терпит неудачу (с counter между 10600 и 10700).

Что не так?

P.S.: я видел этот вопрос, но это не помогает...


person fiktor    schedule 26.01.2014    source источник
comment
обратите внимание, что ваши 2 if не заботятся обо всех числах в диапазоне ]0,1[; вы, вероятно, захотите переосмыслить весь код и то, что он делает. Просто используйте Bernoulli PRNG   -  person user2485710    schedule 26.01.2014
comment
@OliCharlesworth: если бы я просто return rand() < probability*(RAND_MAX+1.0), то я бы получал true из этой функции каждые 1.0/32768 попыток (в среднем) даже для probability = 1.0e-100, что было бы непригодно для моих целей.   -  person fiktor    schedule 26.01.2014
comment
@ user2485710: Да, мои два if не заботятся обо всех возможных числах, возвращаемых rand(). Вот почему у меня в конце есть оператор return, который вызывается с небольшой вероятностью около 1.0/RAND_MAX.   -  person fiktor    schedule 26.01.2014
comment
ваш способ писать и думать об этом в корне неверен, просто подумайте о том факте, что вы объявили double в качестве возвращаемого типа, когда в 2 случаях из 3 вы возвращаете bool.   -  person user2485710    schedule 27.01.2014
comment
@ user2485710: Извините за путаницу, это опечатка.   -  person fiktor    schedule 28.01.2014


Ответы (3)


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

static unsigned int state;

int rand() {
   state = nextState(); /* this actually moves the state from one random value to the next, eg., using a LCG */
   return state % RAND_MAX;  /* biased */
}

Смещение возникает из-за того, что более низкие выходные значения имеют еще одно отображение под модом из состояния. Например, если состояние может иметь значения 0–9 (10 значений), а RAND_MAX равно 3 (значения 0–2), то результатом операции % 3 в зависимости от состояния

Output  State
0       0 3 6 9 
1       1 4 7
2       2 5 8

Результат 0 перепредставлен, потому что он имеет шанс быть выбранным 4/10 по сравнению с 3/10 для других значений.

В качестве примера с более вероятными значениями, если внутреннее состояние RNG является 16-целым числом, а RAND_MAX равно 35767 (как вы упомянули, это на вашей платформе), тогда все значения [0,6000] будут выведены для 3 разных значения состояния, но оставшиеся ~30 000 значений будут выведены только для 2 различных значений состояния — значительное смещение. Этот тип смещения может привести к тому, что значение вашего счетчика будет выше, чем ожидалось (поскольку меньшие, чем универсальные, результаты rand() благоприятствуют условию p_scaled >= 1.

Было бы полезно, если бы вы могли опубликовать точную реализацию rand() на вашей платформе. Если окажется, что это смещение в старших битах, вы можете устранить это, передав значения, полученные от rand(), через хорошую хэш-функцию, но лучший подход, вероятно, состоит в том, чтобы просто использовать высококачественный источник случайных данных. числа, например, Вихрь Мерсенна. У лучшего генератора также будет больший выходной диапазон (эффективный, более высокий RAND_MAX), что означает, что ваш алгоритм будет подвергаться меньшему количеству повторных попыток/меньшей рекурсии.

Даже если реализация среды выполнения Visual Studio страдает от этого дефекта, стоит отметить, что это, вероятно, было, по крайней мере частично, преднамеренным выбором дизайна — использование RAND_MAX, такого как 35767, которое является относительно простым по отношению к размеру состояния (обычно степени 2), гарантирует лучшая случайность младших битов, поскольку операция % эффективно смешивает биты старшего и младшего разрядов, а наличие смещенных/неслучайных битов младшего разряда на практике часто является более серьезной проблемой, чем небольшое смещение битов старшего разряда из-за вездесущности вызывающего rand() уменьшает диапазон с помощью %, который эффективно использует только младшие биты для модулей, которые являются степенями 2 (также очень распространены).

person BeeOnRope    schedule 27.01.2014

Я попробовал ваш код в Linux, и результаты были довольно приличными. Однако похоже, что вы находитесь в Windows, где RAND_MAX, вероятно, составляет около 32768. Я говорю, поскольку gcc жаловался на Linux, что RAND_MAX+1 приводит к целочисленному переполнению, поэтому мне пришлось добавить приведение.

Так что проблема, скорее всего, в том, что либо RAND_MAX слишком мал, либо реализация rand() в вашей системе не очень хороша.

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

/* change `rand()` to return two concatenated rands */
typedef long long rand_type; /* this type depends on your actual system, you might get away with `int` */
#define BIGGER_RAND_MAX ((RAND_MAX + 2) * RAND_MAX)
rand_type bigger_rand(void)
{
    return (rand_type)rand() * (RAND_MAX + 1) + rand();
}

А затем попробуйте свою программу с этим рандом, который имеет более высокий диапазон. Если проблема не устранена, скорее всего, дело в вашей далеко не случайной функции rand().


Примечание: ваш random_bool должен возвращать bool, а не double! Поскольку вы проверяете double на ноль, это также может быть источником проблемы, когда у вас есть ложные срабатывания, потому что двойное значение может быть не совсем нулем.

person Shahbaz    schedule 26.01.2014
comment
двойной может быть точно равен нулю. было бы обидно, если бы этого не могло быть. - person Karoly Horvath; 26.01.2014
comment
@KarolyHorvath, конечно может быть. Но он также может быть почти нулевым. Маловероятно в этом случае, так как он возвращается как false и не вычисляется, но в любом случае лучше избегать сравнения double с точными числами. - person Shahbaz; 26.01.2014
comment
вряд ли?? это будет ровно ноль.... и да, очевидно, что эта функция должна была вернуть bool. - person Karoly Horvath; 26.01.2014
comment
@KarolyHorvath, ты абсолютно прав. Раньше я вряд ли был немного консервативен, так как люди обычно публикуют более простую версию своего фактического кода, в котором они могли сделать какие-то другие неправильные вещи. Оставим этот разговор, уверяю вас, я тоже понимаю, что происходит под капотом. - person Shahbaz; 27.01.2014
comment
@Shahbaz: я запускал это в Windows, и RAND_MAX действительно был 35767. Однако я учел конечный RAND_MAX, и теоретически моя функция работает правильно. Но только в теории. Позвольте мне объяснить: представьте систему счисления на основе (RAND_MAX+1), поэтому каждая цифра от 0 до RAND_MAX. Что я делаю, так это, по сути, генерирую действительное число в [0,1], генерирую каждую цифру с помощью rand(), и если оно меньше вероятности --- возвращаю true. Может показаться, что это вызовет функцию rand() бесконечно много раз, но на самом деле это не так: в большинстве случаев мне нужна только первая цифра. Почему эта теория не работает? - person fiktor; 28.01.2014
comment
@fiktor, честно говоря, не знаю. Вот почему я предложил способ поэкспериментировать с ним. Если вы искусственно увеличите RAND_MAX, а проблема не исчезнет, ​​значит, Windows rand() отстой (не сюрприз для Microsoft). Если проблема исчезнет, ​​тогда либо объединение двух rand() приведет к более единообразной функции rand(), либо проблема действительно будет RAND_MAX слишком маленькой. Хотя, скорее всего, это rand() в Windows - отстой. - person Shahbaz; 28.01.2014

я думаю, что результат этой функции связан со значением RAND_MAX, в данном случае p = 1e-6, если RAND_MAX равен 9999, то это никогда не вернет true

person zhanglongpan    schedule 26.01.2014
comment
Я думаю, что это будет. Пусть p=1e-6, RAND_MAX=9999. Тогда с вероятностью 1.0/10000 rand() вернет 0. В этом случае оба if потерпят неудачу, поэтому функция будет рекурсивно вызывать себя с p = 1e-6 * 10000 = 0.01. При второй попытке он вернет true, если rand() вернет что-то из {0,...,99}, и false в противном случае, что дает общую вероятность true, равную (1.0/10000)*(100/10000)=1e-6. Теоретически. - person fiktor; 26.01.2014
comment
эм... вы правы. Я боюсь, что эта rand() функция может генерировать [0,RAND_MAX] случайным образом. - person zhanglongpan; 26.01.2014