9.5 – Генерирование случайных чисел

Добавлено 3 июня 2021 в 21:55

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

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

Однако компьютеры не предназначены для использования преимуществ физических переменных – ваш компьютер не может подбрасывать монету, бросать кости или тасовать реальные карты. Компьютеры живут в управляемом электрическом мире, где всё двоично (ложь или истина) и нет промежуточного состояния. По самой своей природе компьютеры предназначены для получения максимально предсказуемых результатов. Когда вы говорите компьютеру вычислить 2 + 2, вы всегда хотите, чтобы ответ был 4. А не 3 или 5 в отдельных случаях.

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

Генератор псевдослучайных чисел (ГПСЧ или PRNG, pseudo-random number generator) – это программа, которая принимает начальное число (англ. seed) и выполняет над ним математические операции, чтобы преобразовать его в какое-то другое число, которое, кажется, не связано с начальным числом. Затем он берет это сгенерированное число и выполняет с ним ту же математическую операцию, чтобы преобразовать его в новое число, не связанное с числом, из которого оно было сгенерировано. Постоянно применяя алгоритм к последнему сгенерированному числу, он может генерировать последовательность новых чисел, которые будут казаться случайными, если алгоритм достаточно сложен.

Лучшая практика


Предоставить начальное значение своему генератору случайных чисел вы должны только один раз. Если ввести его более одного раза, результаты будут менее случайными или совсем не случайными.

На самом деле написать ГПСЧ довольно просто. Вот короткая программа, которая генерирует 100 псевдослучайных чисел:

#include <iostream>
 
unsigned int PRNG()
{
    // наше начальное число - 5323
    static unsigned int seed{ 5323 };
 
    // Берём текущее порождающее значение и генерируем из него новое значение
    // Из-за того, что мы используем большие константы и переполнение,
    // будет сложно случайно предсказать, какое получится следующее число
    // из предыдущего.
    seed = 8253729 * seed + 2396403;
 
    // Берём число и возвращаем значение от 0 до 32767
    return seed % 32768;
}
 
int main()
{
    // Напечатать 100 случайных чисел
    for (int count{ 1 }; count <= 100; ++count)
    {
        std::cout << PRNG() << '\t';
 
        // Если мы напечатали 5 чисел, начинаем новую строку
        if (count % 5 == 0)
            std::cout << '\n';
    }
 
    return 0;
}

Результат этой программы:

23070   27857   22756   10839   27946
11613   30448   21987   22070   1001
27388   5999    5442    28789   13576
28411   10830   29441   21780   23687
5466    2957    19232   24595   22118
14873   5932    31135   28018   32421
14648   10539   23166   22833   12612
28343   7562    18877   32592   19011
13974   20553   9052    15311   9634
27861   7528    17243   27310   8033
28020   24807   1466    26605   4992
5235    30406   18041   3980    24063
15826   15109   24984   15755   23262
17809   2468    13079   19946   26141
1968    16035   5878    7337    23484
24623   13826   26933   1480    6075
11022   19393   1492    25927   30234
17485   23520   18643   5926    21209
2028    16991   3634    30565   2552
20971   23358   12785   25092   30583

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

Генерация случайных чисел в C++

C (и, как следствие, C++) поставляется со встроенным генератором псевдослучайных чисел. Он реализован как две отдельные функции, которые находятся в заголовке cstdlib:

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

std::rand() генерирует следующее случайное число в последовательности. Это число будет псевдослучайным целым числом от 0 до RAND_MAX, значение константы в cstdlib, которое обычно установлена ​​на 32767.

Вот пример программы, использующей эти функции:

#include <iostream>
#include <cstdlib> // для std::rand() и std::srand()
 
int main()
{
    std::srand(5323); // устанавливаем начальное порождающее значение 5323
    // Из-за недостатков некоторых компиляторов нам нужно вызвать std::rand()
    // один раз здесь, чтобы получить "лучшие" случайные числа.
    std::rand();
 
    // Вывести 100 случайных чисел
    for (int count{ 1 }; count <= 100; ++count)
    {
        std::cout << std::rand() << '\t';
 
        // Если мы напечатали 5 чисел, начинаем новую строку
        if (count % 5 == 0)
            std::cout << '\n';
	}
 
    return 0;
}

Вот результат этой программы:

17421	8558	19487	1344	26934	
7796	28102	15201	17869	6911	
4981	417	    12650	28759	20778	
31890	23714	29127	15819	29971	
1069	25403	24427	9087	24392	
15886	11466	15140	19801	14365	
18458	18935	1746	16672	22281	
16517	21847	27194	7163	13869	
5923	27598	13463	15757	4520	
15765	8582	23866	22389	29933	
31607	180	    17757	23924	31079	
30105	23254	32726	11295	18712	
29087	2787	4862	6569	6310	
21221	28152	12539	5672	23344	
28895	31278	21786	7674	15329	
10307	16840	1645	15699	8401	
22972	20731	24749	32505	29409	
17906	11989	17051	32232	592	
17312	32714	18411	17112	15510	
8830	32592	25957	1269	6793

Последовательности ГПСЧ и заполнение

Если вы запустите приведенный выше пример программы с std::rand() несколько раз, вы заметите, что она каждый раз выводит один и тот же результат! Это означает, что хотя каждое число в последовательности кажется случайным по сравнению с предыдущими, вся последовательность вовсе не случайна! А это означает, что наша программа оказывается полностью предсказуемой (одни и те же входные данные всегда приводят к одним и тем же выходным данным). Бывают случаи, когда это может быть полезно или даже желательно (например, вы хотите, чтобы научное моделирование повторялось, или вы пытаетесь выяснить, почему ваш генератор случайных подземелий дает сбой).

Но часто это не то, что нужно. Если вы пишете игру «hi-lo» (высоко-низко) (где у пользователя есть 10 попыток угадать число, а компьютер сообщает ему, является ли его предположение слишком высоким или слишком низким), вы не хотите, чтобы программа каждый раз выбирала одни и те же числа. Итак, давайте подробнее посмотрим, почему это происходит, и как мы можем это исправить.

Помните, что каждое число в последовательности ГПСЧ определенным образом генерируется из предыдущего числа. Таким образом, при любом одном и том же начальном числе ГПСЧ всегда будут генерировать одну и ту же последовательность чисел! Мы получаем ту же последовательность, потому что наше начальное число всегда 5323.

Чтобы сделать всю нашу последовательность случайной, нам нужен способ выбрать начальное число, которое не является фиксированным значением. Первый ответ, который, вероятно, приходит в голову, – нам нужно случайное число! Это хорошая мысль, но если нам нужно случайное число для генерации случайных чисел, тогда мы попадаем в уловку-22. Оказывается, нам на самом деле не нужно, чтобы наше начальное число было случайным – нам просто нужно выбирать что-то, что меняется при каждом запуске программы. Затем мы можем использовать наш ГПСЧ для генерации уникальной последовательности псевдослучайных чисел из этого начального числа.

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

C поставляется с функцией std::time(), которая возвращает количество секунд с полуночи 1 января 1970 года. Чтобы использовать ее, нам просто нужно включить заголовок ctime, а затем инициализировать std::srand() с помощью вызов std::time(nullptr). Мы еще не рассмотрели nullptr, но в данном контексте это эквивалент 0.

Вот та же программа, что и выше, но с вызовом time() в качестве начального числа:

#include <iostream>
#include <cstdlib> // для std::rand() и std::srand()
#include <ctime>   // для std::time()
 
int main()
{
    // устанавливаем начальное значение в значение системных часов
    std::srand(static_cast<unsigned int>(std::time(nullptr)));
 
    for (int count{ 1 }; count <= 100; ++count)
    {
        std::cout << std::rand() << '\t';
 
        // Если мы напечатали 5 чисел, начинаем новую строку
        if (count % 5 == 0)
            std::cout << '\n';
	}
 
    return 0;
}

Теперь наша программа будет каждый раз генерировать новую последовательность случайных чисел! Запустите ее пару раз и убедитесь сами.

Генерация случайных чисел между двумя произвольными значениями

Как правило, нам не нужны случайные числа от 0 до RAND_MAX – нам нужны числа между двумя другими значениями, которые мы назовем min и max. Например, если мы пытаемся имитировать, как пользователь бросает кубик, нам нужны случайные числа от 1 до 6.

Вот короткая функция, которая преобразует результат rand() в нужный диапазон:

// Генерируем случайное число от min до max (включительно)
// Предполагается, что std::srand() уже был вызван
// Предполагается, что max - min <= RAND_MAX
int getRandomNumber(int min, int max)
{
    // static используется для повышения эффективности, поэтому
    // мы вычисляем это значение только один раз
    static constexpr double fraction { 1.0 / (RAND_MAX + 1.0) };  
    // равномерно распределяем случайные числа по нашему диапазону
    return min + static_cast<int>((max - min + 1) * (std::rand() * fraction));
}

Чтобы смоделировать бросок кубика, мы должны вызвать getRandomNumber(1, 6). Чтобы выбрать случайную цифру, мы вызываем getRandomNumber(0, 9).

Дополнительный материал: как работает предыдущая функция?

Функция getRandomNumber() может показаться немного сложной, но всё не так уж плохо.

Вернемся к нашей цели. Функция rand() возвращает число от 0 до RAND_MAX (включительно). Мы хотим каким-то образом преобразовать результат rand() в число от min до max (включительно). Это означает, что когда мы выполняем преобразование, 0 должен стать min, а RAND_MAX должен стать max с равномерным распределением чисел между ними.

Мы делаем это в пять этапов:

  1. Умножаем наш результат от std::rand() на дробь fraction. Это преобразует результат rand() в число с плавающей запятой от 0 (включительно) до 1 (не включая).
  2. Если rand() возвращает 0, тогда 0 * fraction по-прежнему равно 0. Если rand() возвращает RAND_MAX, тогда RAND_MAX * fraction равно RAND_MAX / (RAND_MAX + 1), что немного меньше 1. Любые другие числа, возвращаемые функцией rand() будут равномерно распределены между этими двумя точками.
  3. Затем нам нужно знать, сколько чисел мы можем вернуть. Другими словами, сколько чисел находится между min (включительно) и max (включительно)?
  4. Это просто (max - min + 1). Например, если max = 8 и min = 5, (max - min + 1) = (8 - 5 + 1) = 4. Между 5 и 8 есть 4 числа (то есть 5, 6, 7 и 8).
  5. Умножаем два предыдущих результата вместе. Если у нас было число с плавающей запятой от 0 (включительно) до 1 (не включая), а затем мы умножили его на (max - min + 1), теперь у нас есть число с плавающей запятой между 0 (включительно) и (max - min + 1) (не включая).
  6. Преобразуем предыдущий результат в целое число. Это удаляет любую дробную составляющую, оставляя нам целочисленный результат от 0 (включительно) до (max - min) (включительно).
  7. Наконец, мы добавляем min, что переводит наш результат в целое число от min (включительно) до max (включительно).

Дополнительный материал: почему в предыдущей функции мы не используем оператор остатка от деления (%)?

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

Давайте посмотрим, что бы произошло, если бы вместо этого приведенная выше функция выглядела так:

return min + (std::rand() % (max-min+1));

Похожа, правда? Давайте разберемся, где всё идет не по плану. Чтобы упростить пример, скажем, что rand() всегда возвращает случайное число от 0 до 9 (включительно). В нашем примере мы выберем min = 0 и max = 6. Таким образом, max - min + 1 равно 7.

Теперь давайте посчитаем все возможные исходы:

0 + (0 % 7) = 0
0 + (1 % 7) = 1
0 + (2 % 7) = 2
0 + (3 % 7) = 3
0 + (4 % 7) = 4
0 + (5 % 7) = 5
0 + (6 % 7) = 6

0 + (7 % 7) = 0
0 + (8 % 7) = 1
0 + (9 % 7) = 2

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

Теперь давайте посмотрим на результат функции getRandomNumber() выше, используя те же параметры, что и выше (rand() возвращает число от 0 до 9 (включительно), min = 0 и max = 6). В этом случае fraction = 1 / (9 + 1) = 0,1. max - min + 1 по-прежнему 7.

Расчет всех возможных исходов:

0 + static_cast(7 * (0 * 0.1))) = 0 + static_cast(0) = 0
0 + static_cast(7 * (1 * 0.1))) = 0 + static_cast(0.7) = 0
0 + static_cast(7 * (2 * 0.1))) = 0 + static_cast(1.4) = 1
0 + static_cast(7 * (3 * 0.1))) = 0 + static_cast(2.1) = 2
0 + static_cast(7 * (4 * 0.1))) = 0 + static_cast(2.8) = 2
0 + static_cast(7 * (5 * 0.1))) = 0 + static_cast(3.5) = 3
0 + static_cast(7 * (6 * 0.1))) = 0 + static_cast(4.2) = 4
0 + static_cast(7 * (7 * 0.1))) = 0 + static_cast(4.9) = 4
0 + static_cast(7 * (8 * 0.1))) = 0 + static_cast(5.6) = 5
0 + static_cast(7 * (9 * 0.1))) = 0 + static_cast(6.3) = 6

Здесь всё еще есть небольшое смещение в сторону меньших чисел (0, 2 и 4 появляются дважды, тогда как 1, 3, 5 и 6 появляются один раз), но результаты распределены гораздо более равномерно.

Несмотря на то, что getRandomNumber() немного сложнее для понимания, чем альтернатива с остатком от деления, мы выступаем за метод с делением, потому что он дает менее предсказуемый результат.

Что такое хороший генератор псевдослучайных чисел?

Как я уже упоминал выше, ГПСЧ, который мы написали, не очень хороший. В этом разделе мы обсудим причины, почему. Это дополнительный материал, потому что он не имеет прямого отношения к C или C++, но если вам нравится программировать, вам, вероятно, всё равно будет интересно.

Чтобы быть хорошим, ГПСЧ должен обладать рядом свойств:

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

Например, предположим, вы пытаетесь написать генератор случайных предметов для игры. Вы выбираете случайное число от 1 до 10, и если результат равен 10, с монстра выпадет мощный предмет вместо обычного. Вы ожидаете, что это произойдет с вероятностью 1 из 10. Но если результаты простого ГПСЧ распределены неравномерно, и он генерирует намного больше десяток, чем должен, ваши игроки в конечном итоге получат больше редких предметов, чем вы предполагали, что, возможно, упростит вашу игру.

Создание ГПСЧ, дающих равномерно распределенные результаты, является сложной задачей, и это одна из основных причин, по которой ГПСЧ, который мы написали в начале этого урока, не очень хороший.

Во-вторых, метод, с помощью которого генерируется следующее число в последовательности, не должен быть очевидным или предсказуемым. Например, рассмотрим следующий алгоритм ГПСЧ: число = число + 1. Результаты этого ГПСЧ идеально равномерно распределены, но они не очень полезны в качестве последовательности случайных чисел!

В-третьих, ГПСЧ должен иметь хорошее пространственное распределение чисел. Это означает, что он должен возвращать низкие, средние и высокие значения, казалось бы, случайным образом. ГПСЧ, который вернул все низкие значения, а затем все высокие значения, может быть равномерно распределенным и непредсказуемыми, но он всё равно приведет к смещенным результатам, особенно если количество случайных чисел, которые вы реально используете, невелико.

В-четвертых, все ГПСЧ являются периодическими, что означает, что в какой-то момент сгенерированная последовательность чисел начнет повторяться. Длина последовательности до того, как ГПСЧ начинает повторяться, называется периодом.

Например, вот первые 100 чисел, сгенерированных из ГПСЧ с плохой периодичностью:

112	  9	    130	  97	64	
31	  152	119	  86	53	
20	  141	108	  75	42	
9	  130	97	  64	31	
152	  119	86	  53	20	
141	  108	75	  42	9	
130	  97	64	  31	152	
119	  86	53	  20	141	
108	  75	42	  9	    130	
97	  64	31	  152	119	
86	  53	20	  141	108	
75	  42	9	  130	97	
64 	  31	152	  119	86	
53	  20	141	  108	75	
42	  9	    130	  97	64	
31	  152	119	  86	53	
20	  141	108	  75	42	
9	  130	97	  64	31	
152	  119	86	  53	20	
141	  108	75	  42	9

Вы можете заметить, что он сгенерировал 9 как второе число и снова 9 как 16-е число. ГПСЧ застревает, постоянно генерируя последовательность между этими двумя девятками: 9-130-97-64-31-152-119-86-53-20-141-108-75-42- (повтор).

Это происходит потому, что ГПСЧ детерминированы – при некотором наборе входных значений они каждый раз будут выдавать одно и то же выходное значение. Это означает, что как только ГПСЧ встречает набор входных данных, которые он использовал ранее, он начинает производить ту же последовательность выходных данных, которую он создавал раньше, что приводит к циклу.

Хороший ГПСЧ должен иметь длительный период для всех начальных значений. Разработка алгоритма, отвечающего этому свойству, может быть чрезвычайно сложной задачей – большинство ГПСЧ будут иметь длительные периоды для одних начальных значений и короткие периоды для других. Если пользователь выберет начальное число с коротким периодом, тогда ГПСЧ не будет работать хорошо.

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

std::rand() – посредственный ГПСЧ

Алгоритм, используемый для реализации std::rand(), может варьироваться от компилятора к компилятору, что приводит к результатам, которые могут не совпадать между компиляторами. В большинстве реализаций rand() используется метод, называемый линейным конгруэнтным генератором (LCG, Linear Congruential Generator). Если вы посмотрите на первый пример в этом уроке, вы заметите, что на самом деле это LCG, хотя и с намеренно выбранными плохими константами. LCG, как правило, имеют недостатки, из-за которых они не подходят для решения большинства проблем.

Одним из основных недостатков rand() является то, что RAND_MAX обычно устанавливается равным 32767 (по сути, 15 бит). Это означает, что если вы хотите генерировать числа в большем диапазоне (например, 32-битные целые числа), rand() не подходит. Кроме того, rand() не подходит, если вы хотите генерировать случайные числа с плавающей запятой (например, от 0,0 до 1,0), что часто бывает полезно при статистическом моделировании. Наконец, rand() имеет относительно короткий период по сравнению с другими алгоритмами.

Тем не менее, rand() идеально подходит для обучения программированию и для программ, в которых высококачественный ГПСЧ не является необходимостью.

Для приложений, где полезен высококачественный ГПСЧ, я бы порекомендовал вихрь Мерсенна (Mersenne Twister) (или один из его вариантов), который дает отличные результаты и относительно прост в использовании. Вихрь Мерсенна был введен в C++11, и мы покажем, как его использовать позже в этом уроке.

Отладка программ, использующих случайные числа

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

По этой причине при отладке полезно установить случайное начальное число (через std::srand) на определенное значение (например, 0), которое вызывает ошибочное поведение. Это гарантирует, что ваша программа каждый раз будет генерировать одни и те же результаты, что упростит отладку. Как только вы обнаружите ошибку, вы можете снова использовать системные часы, чтобы снова начать генерировать рандомизированные результаты.

Получение лучших случайных чисел с помощью вихря Мерсенна

В C++11 в стандартную библиотеку C++ добавлено множество функций генерации случайных чисел, включая алгоритм вихря Мерсенна, а также генераторы для различных типов случайных распределений (равномерного, нормального, пуассоновского и т.д.). Доступ к нему осуществляется через заголовок <random>.

Вот небольшой пример, показывающий, как сгенерировать случайные числа в C++11 с помощью вихря Мерсенна:

#include <iostream>
#include <random> // для std::mt19937
#include <ctime>  // для std::time
 
int main()
{
	// Инициализируем наш вихрь Мерсенна случайным начальным значением на основе часов
	std::mt19937 mersenne{ static_cast<std::mt19937::result_type>(std::time(nullptr)) };
 
	// Создаем многоразовый генератор случайных чисел,
    //  который равномерно генерирует числа от 1 до 6
	std::uniform_int_distribution die{ 1, 6 };
	// Если ваш компилятор не поддерживает C++17, вместо этого используйте следующее
	// std::uniform_int_distribution<> die{ 1, 6 };
 
	// Распечатываем пачку случайных чисел
	for (int count{ 1 }; count <= 48; ++count)
	{
		std::cout << die(mersenne) << '\t'; // здесь генерируем результат броска кубика
 
		// Если мы напечатали 6 чисел, начинаем новую строку
		if (count % 6 == 0)
			std::cout << '\n';
	}
 
	return 0;
}

Примечание автора


До C++17 вам нужно было после типа добавить пустые скобки для создания die
std::uniform_int_distribution<> die{ 1, 6 }

Вы можете заметить, что вихрь Мерсенна генерирует случайные 32-битные целочисленные значения без знака (а не 15-битные целочисленные значения, как std::rand()), что дает гораздо больший диапазон. Также существует версия (std::mt19937_64) для генерации 64-битных целочисленных значений без знака.

Случайные числа в нескольких функциях

В приведенном выше примере генератор псевдослучайных чисел создается для использования в одной функции. Что произойдет, если мы захотим использовать генератор случайных чисел в нескольких функциях?

Хотя вы можете создать статическую локальную переменную std::mt19937 в каждой функции, которая в ней нуждается (статическая, чтобы она была инициализирована только один раз); но это немного излишне – чтобы каждая функция получала начальное значение для генератора случайных чисел и поддерживала свой собственный локальный генератор. В большинстве случаев лучшим вариантом является создание глобального генератора случайных чисел (внутри пространства имен!). Помните, как мы говорили вам избегать неконстантных глобальных переменных? Это исключение (также обратите внимание: std::rand() и std::srand() обращаются к глобальному объекту, поэтому для этого есть прецедент).

#include <iostream>
#include <random> // для std::mt19937
#include <ctime>  // для std::time
 
namespace MyRandom
{
	// Инициализируем наш вихрь Мерсенна случайным значением на основе часов
    // (один раз при запуске программы)
	std::mt19937 mersenne{ static_cast<std::mt19937::result_type>(std::time(nullptr)) };
}
 
int getRandomNumber(int min, int max)
{
    // мы можем создать распределение в любой функции, которая в нем нуждается
	std::uniform_int_distribution die{ min, max }; 
    // а затем генерировать случайное число из нашего глобального генератора
	return die(MyRandom::mersenne); 
}
 
int main()
{
	std::cout << getRandomNumber(1, 6) << '\n';
	std::cout << getRandomNumber(1, 10) << '\n';
	std::cout << getRandomNumber(1, 20) << '\n';
 
	return 0;
}

Использование библиотеки генерирования случайных чисел

Возможно, лучшее решение – использовать стороннюю библиотеку, которая обрабатывает всё это за вас, например, библиотеку генерирования случайных чисел Effolkronium, использующую только заголовочные файлы. Вы просто добавляете заголовочный файл в свой проект, включаете его с помощью #include, после чего можете начать генерировать случайные числа с помощью Random::get(min, max).

Вот приведенная выше программа, модифицированная под использование библиотеки Effolkronium:

#include <iostream>
#include "random.hpp"
 
// получаем псевдоним генератора случайных чисел, который автоматически
// заполняется и имеет статические API и внутреннее состояние
using Random = effolkronium::random_static;
 
int main()
{
	std::cout << Random::get(1, 6) << '\n';
	std::cout << Random::get(1, 10) << '\n';
	std::cout << Random::get(1, 20) << '\n';
 
	return 0;
}

Помогите! Мой генератор случайных чисел генерирует одну и ту же последовательность случайных чисел!

Если ваш генератор случайных чисел при каждом запуске вашей программы генерирует одну и ту же последовательность случайных чисел, вы, вероятно, неправильно его инициализировали. Убедитесь, что вы инициализируете его значением, которое изменяется при каждом запуске программы (например, std::time(nullptr)).

Помогите! Мой генератор случайных чисел всегда генерирует одно и то же первое число!

Реализация rand() в Visual Studio и некоторых других компиляторах имеет недостаток – первое сгенерированное случайное число не сильно меняется для похожих начальных значений. Это означает, что при использовании std::time(nullptr) для инициализации вашего генератора случайных чисел первый результат rand() не сильно изменится при последующих запусках. Однако на результаты последующих вызовов rand() это не повлияет, и они будут достаточно рандомизированы.

Решение здесь, и хорошее практическое правило в целом, – отбросить первое число, сгенерированное генератором случайных чисел.

Помогите! Мой генератор случайных чисел вообще не генерирует случайные числа!

Если ваш генератор случайных чисел генерирует одно и то же число каждый раз, когда вы запрашиваете случайное число, то вы, вероятно, либо повторно инициализируете генератор случайных чисел перед генерацией случайного числа, либо создаете новый генератор случайных чисел для каждого получения случайного числа.

Вот две функции, которые показывают проблему:

// Это та же функция, которую мы показали ранее
int getRandomNumber(int min, int max)
{
    static constexpr double fraction { 1.0 / (RAND_MAX + 1.0) };
    return min + static_cast<int>((max - min + 1) * (std::rand() * fraction));
}
 
int rollDie()
{
    std::srand(static_cast<unsigned int>(std::time(nullptr)));
    return getRandomNumber(1, 6);
}
 
int getOtherRandomNumber()
{
    std::mt19937 mersenne{ static_cast<std::mt19937::result_type>(std::time(nullptr)) };
    std::uniform_int_distribution rand{ 1, 52 };
    return rand(mersenne);
}

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

В верхнем случае std::srand() повторно инициализирует встроенный генератор случайных чисел перед вызовом rand() (с помощью getRandomNumber()).

В нижнем случае мы создаем новый вихрь Мерсенна, инициализируем его, генерируем одно случайное число и затем уничтожаем его.

Для получения случайных результатов вы должны инициализировать генератор случайных чисел только один раз (обычно при инициализации программы для std::srand() или в точке создания для других генераторов случайных чисел), а затем использовать тот же генератор случайных чисел для каждого последующего генерируемого случайного числа.

Предупреждение


Пример getOtherRandomNumber() – одна из самых распространенных ошибок, которые допускаются в тестах. Вы не заметите, что getOtherRandomNumber() не работает, пока вы не начнете вызывать его чаще, чем один раз в секунду (поскольку начальное число изменяется только один раз в секунду). Не забудьте сделать генератор случайных чисел статическим или объявить его вне функции.

Теги

C++ / CppLearnCppВихрь Мерсенна / Mersenne TwisterГенератор псевдослучайных чисел (ГПСЧ) / Pseudo-random number generator (PRNG)Для начинающихОбучениеПрограммирование

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

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.