Чому люди кажуть, що при використанні генератора випадкових чисел існує модульне зміщення?


277

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

Відповіді:


394

Так 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 . Тому Р (0) = 4/11

Коли rand()повертається 1, 4, 7 або 10 rand()%3 == 1 ,. Тому Р (1) = 4/11

Коли rand()повертається 2, 5 або 8 rand()%3 == 2 ,. Тому Р (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().


Праці, цитовані та читати далі:



6
Інший спосіб мислення about_ RAND_MAX%n == n - 1_ це (RAND_MAX + 1) % n == 0. Читаючи код, я схильний розуміти % something == 0"рівномірно поділене" легше, ніж інші способи його обчислення. Звичайно, якщо ваш C ++ stdlib має RAND_MAXтаке ж значення, як INT_MAX, (RAND_MAX + 1)напевно, він би не працював; тому розрахунок Марка залишається найбезпечнішим виконанням.
Сліпп Д. Томпсон

дуже приємна відповідь!
Sayali Sonawane

Я, можливо, забиваю, але якщо мета - зменшити витрачені шматочки, ми могли б трохи покращити це для крайової умови, коли RAND_MAX (RM) лише на 1 менше, ніж бути однаково поділеним на N. У цьому сценарії жодних бітів не потрібно витрачати робити X> = (RM - RM% N)), що має малі значення для малих значень N, але стає більшим значенням для великих значень N. Як згадував Сліпп Д. Томпсон, є рішення, яке буде працювати тільки коли INT_MAX (IM)> RAND_MAX, але розривається, коли вони рівні. Однак для цього є просте рішення, ми можемо змінити обчислення X> = (RM - RM% N) наступним чином:
Бен Персонік

X> = RM - (((RM% N) + 1)% N)
Бен Персонік

Я опублікував додаткову відповідь, де детально пояснив проблему і наводя приклад рішення коду.
Бен Персонік

36

Продовжуйте вибирати випадковий вибір - хороший спосіб усунути упередження.

Оновлення

Ми могли б зробити код швидким, якщо шукати х у діапазоні, що ділиться на 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 ітерація.


2
Yuck :-P перетворення на подвійне, а потім множення на MAX_UPPER_LIMIT / RAND_MAX набагато чистіше і краще.
хлопець

22
@boycy: ви пропустили суть. Якщо кількість значень, які rand()можна повернути, не кратна n, то, що б ви не зробили, ви неминуче отримаєте «модульний зміщення», якщо не відкинете деякі з цих значень. user1413793 пояснює це чудово (хоча рішення, запропоноване у цій відповіді, справді щасливе).
TonyK

4
@TonyK мої вибачення, я пропустив суть. Не думав досить важко, і вважав, що упередження застосовуватиметься лише з методами, що використовують операцію з явним модулем. Дякую, що
виправили

Пріоритет оператора робить RAND_MAX+1 - (RAND_MAX+1) % nроботу правильно, але я все ж думаю, що це слід писати як RAND_MAX+1 - ((RAND_MAX+1) % n)для ясності.
Лінус Арвер

4
Це не спрацює, якщо RAND_MAX == INT_MAX (як це робиться в більшості систем) . Дивіться мій другий коментар до @ user1413793 вище.
BlueRaja - Danny Pflughoeft

19

@ user1413793 вірно вирішує проблему. Я не збираюся обговорювати це далі, за винятком одного питання: так, для малих значень nта великих значень RAND_MAXмодульний зміщення може бути дуже малим. Але використання шаблону, що викликає зміщення, означає, що ви повинні враховувати зміщення кожного разу, коли ви обчислюєте випадкове число і вибираєте різні шаблони для різних випадків. І якщо ви зробите неправильний вибір, помилки, які він вводить, є тонкими і майже неможливими для тестування. У порівнянні з просто використанням відповідного інструменту (наприклад arc4random_uniform), це додаткова робота, а не менша робота. Робити більше роботи та отримувати гірше рішення - це жахлива інженерія, особливо коли робити це правильно щоразу на більшості платформ.

На жаль, реалізація рішення є некоректною або менш ефективною, ніж повинна бути. (Кожне рішення має різні коментарі, що пояснюють проблеми, але жодне з рішень не було виправлене для їх вирішення.) Це, ймовірно, може заплутати випадкового шукача відповіді, тому я пропоную тут добре відому реалізацію.

Знову ж таки, найкращим рішенням є просто використання arc4random_uniformна платформах, які її надають, або аналогічне рішення для вашої платформи (наприклад, Random.nextIntна Java). Це зробить правильно, без коду для вас. Це майже завжди правильний дзвінок.

Якщо у вас немає arc4random_uniform, то ви можете використовувати потужність openource, щоб точно побачити, як він реалізується поверх RNG широкого діапазону ( ar4randomу цьому випадку, але подібний підхід може працювати і над іншими RNG).

Ось реалізація 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;
 }

Зауважимо, що якщо arcfour_random() реально використовувати алгоритм справжнього RC4 при його реалізації, то висновок, безумовно, матиме деякий ухил. Сподіваємось, автори вашої бібліотеки перейшли на використання кращої CSPRNG за тим самим інтерфейсом. Я пригадую, один з BSD зараз фактично використовує алгоритм ChaCha20 для реалізації arcfour_random(). Детальніше про упередження виводу RC4, які роблять його марним для безпеки та інших критичних додатків, таких як відеопокер: blog.cryptographyengineering.com/2013/03/…
rmalayter

2
@rmalayter В iOS та OS X arc4random зчитується з / dev / random, що є ентропією найвищої якості в системі. (Назва "arc4" є історичною і збереглася для сумісності.)
Rob Napier

@Rob_Napier добре знати, але /dev/randomв минулому також використовував RC4 на деяких платформах (Linux використовує SHA-1 у протиборковому режимі). На жаль, сторінки чоловіка, які я знайшов за допомогою пошуку, вказують, що RC4 все ще використовується на різних платформах, які пропонують arc4random(хоча фактичний код може бути різним).
rmalayter

1
Я збентежений. Чи не -upper_bound % upper_bound == 0??
Джон МакКлунг

1
@JonMcClung -upper_bound % upper_boundдійсно буде 0, якщо intвін ширший за 32 біт. Це повинно бути (u_int32_t)-upper_bound % upper_bound)(припускаючи u_int32_t, що це BSD-ism для uint32_t).
Ян Абботт

14

Визначення

Modulo Bias - це властивий ухил використання модуля арифметики для зменшення набору виходів до підмножини вхідного набору. Взагалі, зміщення існує, коли відображення між входом і набором набору розподіляється не однаково, як у випадку використання арифметики по модулю, коли розмір набору виходів не є дільником розміру вхідного набору.

Такого зміщення особливо важко уникнути при обчисленні, де числа представлені у вигляді рядків бітів: 0s та 1s. Знайти справді випадкові джерела випадковості також надзвичайно важко, але виходить за межі цієї дискусії. У решті цієї відповіді припустимо, що існує необмежене джерело справді випадкових бітів.

Приклад проблеми

Розглянемо моделювання рулону штампу (від 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 для подачі випадкових байтів. Під час компіляції обов'язково посилайтеся на бібліотеку, -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значеннями, щоб побачити, скільки повторних роликів насправді відбувається за більшості умов. Скептично налаштована особа також може зберегти обчислювані значення для файлу та переконатися, що розподіл виглядає нормальним.


Я дуже сподіваюся, що ніхто не сліпо скопіював вашу єдину випадкову реалізацію. randomPool = RAND_bytes(...)Лінія завжди буде призводити до randomPool == 1зв'язку з затвердженням. Це завжди призводить до відмови та повторного прокату. Я думаю, ви хотіли заявити в окремому рядку. Отже, це призвело до повернення RNG 1за кожну ітерацію.
Qix - МОНІКА ПОМИЛИЛА

Щоб було зрозуміло, randomPoolзавжди буде оцінюватися 1відповідно до документаціїRAND_bytes() OpenSSL, оскільки це завжди буде успішним завдяки RAND_status()твердженню.
Qix - МОНІКА ПОМИЛИЛА

9

Є дві звичні скарги на використання модуля.

  • одна діє для всіх генераторів. У лімітному випадку це легше побачити. Якщо ваш генератор має RAND_MAX, який дорівнює 2 (що не відповідає стандарту C), і ви хочете лише 0 або 1 як значення, використання модуля генерує 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)

BTW, C ++ 11 запровадили стандартні способи скорочення та інші генератори, ніж rand ().


n == RAND_MAX? 1: (RAND_MAX-1) / (n + 1): я розумію, що ідея полягає в тому, щоб спочатку розділити RAND_MAX на рівний розмір сторінки N, потім повернути відхилення в межах N, але я не можу точно вказати код на це.
цинкування

1
Наївна версія повинна бути (RAND_MAX + 1) / (n + 1), оскільки є значення RAND_MAX + 1, які потрібно розділити на n + 1 відра. Якщо уникнути переповнення при обчисленні RAND_MAX + 1, він може бути перетворений в 1+ (RAND_MAX-n) / (n + 1). Щоб уникнути переповнення при обчисленні n + 1, спочатку перевіряється випадок n == RAND_MAX.
AProgrammer

+ плюс, робити поділ, здається, коштує дорожче, порівняно з новим числом.
цинкування

4
Взяття за модулем і поділ мають однакову вартість. Деякі ISA навіть пропонують лише одну інструкцію, яка забезпечує завжди і те, і інше. Вартість відновлення чисел буде залежати від n та RAND_MAX. Якщо n невеликий щодо RAND_MAX, це може коштувати дорого. І очевидно, ви можете вирішити, що ухили не важливі для вашої заявки; Я просто даю спосіб уникнути їх.
AProgrammer

9

Рішення Марка (прийняте рішення) майже ідеально.

int x;

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

x %= n;

відредаговано 25 березня 1616 о 23:16

Марк Амери 39k21170211

Однак він має застереження, яке відкидає 1 дійсний набір результатів у будь-якому сценарії, де RAND_MAX( RM) на 1 менше кратного N(Де N= Кількість можливих дійсних результатів).

тобто коли 'число відхилених значень' ( D) дорівнює N, то вони насправді є дійсним набором (а V)не недійсним набором ( I)

Причиною цього є те, що в якийсь момент Марк втрачає вигляд різниці між Nі Rand_Max.

Nце набір, дійсні члени якого складаються лише з позитивних цілих чисел, оскільки він містить кількість відповідей, які були б дійсними. (наприклад: Set 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%

Оскільки відсоток необхідних Rerolls збільшується, чим ближче 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

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

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

Хіба не можна сказати, що проблема рішення Марка полягає в тому, що він розглядає RAND_MAX і n як одну і ту ж "одиницю вимірювання", коли насправді вони мають на увазі дві різні речі? У той час як n представляє результуючу "кількість можливостей", RAND_MAX представляє лише максимальне значення вихідної можливості, де RAND_MAX + 1 буде вихідним числом можливостей. Я здивований, що він не дійшов до вашого висновку, оскільки він, схоже, визнав, що n і RAND_MAX не були однаковими з рівнянням:RAND_MAX%n = n - 1
Даніло Суза Морєс,

@ DaniloSouzaMorães Дякую Данило, ви поставили справу дуже лаконічно. Я хотів продемонструвати, що він робить разом з Чому і як це робити, але не думаю, що я ніколи не міг заявити, ЩО він робив неправильно красномовно, оскільки я так заплутаний у деталях логіки про те, як і чому виникає питання, що я не так чітко заявляю, про що йдеться. Чи не заперечуєте ви, якщо я вношу поправки у свою відповідь, щоб використати щось із написаного тут як власне резюме до питання про те, що і де прийняте рішення робить те, що потрібно вирішити біля верху?
Бен

Це було б чудово. Перейдіть на це
Даніло Суза Мораес

1

При 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

-1

Як свідчить прийнята відповідь , "зміщення модуля" має коріння в низькому значенні 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.

Набагато кращим рішенням, ніж циклічне циклічне використання (яке шалено неефективне і навіть не слід пропонувати), - використовувати PRNG зі значно більшим діапазоном випуску. Алгоритм Mersenne Twister має максимальний вихід 4 294 967 295. Оскільки це робиться MersenneTwister::genrand_int32() % 10для всіх намірів і цілей, буде розподілено рівномірно і ефект зміщення модуля зникне, але зникне.


3
Ваш є більш ефективним, і, ймовірно, правда, що якщо RAND_MAX значно більший, ніж число, на яке ви модеруєте, проте ваше все одно буде упереджено. Зрозуміло, що це все генератори псевдовипадкових чисел у будь-якому разі, що саме по собі є іншою темою, але якщо ви вважаєте повністю генератор випадкових чисел, ваш шлях все одно зміщує нижчі значення.
користувач1413793

Оскільки найвище значення непарне, MT::genrand_int32()%2вибирає 0 (50 + 2.3e-8)% часу і 1 (50 - 2.3e-8)% часу. Якщо ви не будуєте RGN казино (для якого ви, ймовірно, використовували б набагато більший діапазон RGN), будь-який користувач не збирається помічати зайві 2,3е-8% часу. Ви говорите про занадто малі числа, щоб мати значення тут.
bobobobo

7
Цикл - найкраще рішення. Це не «шалено неефективно»; вимагаючи менше ніж удвічі повторень у гіршому середньому випадку. Використання високого RAND_MAXзначення зменшить зміщення модуля, але не усуне його. Цикл волі.
Джаред Нільсен

5
Якщо кількість RAND_MAXє достатньою, ніж число, на яке ви модеруєте, кількість разів, необхідне для відновлення випадкового числа, зникає на малому рівні і не вплине на ефективність. Я кажу, продовжуйте циклічно, доки ви протестуєте проти найбільшого кратного, nа не просто, nяк запропоновано прийнятою відповіддю.
Марк Викуп 11

-3

Я щойно написав код для методу неупередженого відкидання монет Фон Ноймана, який теоретично повинен усунути будь-які зміщення у процесі генерації випадкових чисел. Більше інформації можна знайти на веб-сайті ( 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;
        }
    }
}

Це не стосується зміщення модуля. Цей процес може бути використаний для усунення зміщення в бітовому потоці. Однак, щоб дістатись від бітового потоку до рівномірного розподілу від 0 до n, де n не менше, ніж потужність двох, потрібна адресація модульного зміщення. Таким чином, це рішення не може усунути будь-яких упереджень у процесі генерації випадкових чисел.
Рік

2
@Rick хм. Логічним розширенням методу Фон Неймана на усунення модульного зміщення при генеруванні випадкового числа між, скажімо, 1 і 100, було б: A) виклик rand() % 100100 разів. Б) якщо всі результати різні, візьміть перший. C) в іншому випадку GOTO A. Це спрацює, але якщо очікувана кількість ітерацій приблизно 10 ^ 42, вам доведеться бути досить терплячими. І безсмертний.
Марк Амері

@MarkAmery Дійсно, що має працювати. Переглядаючи цей алгоритм, хоча він неправильно реалізований. Першим ще має бути:else if(prev==2) prev= x1; else { if(prev!=x1) return prev; prev=2;}
Рік
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.