9.5 – Генерирование случайных чисел
Возможность генерировать случайные числа может быть полезна в определенных видах программ, особенно в играх, программах статистического моделирования и научных симуляторах, которые должны моделировать случайные события. Возьмем, к примеру, игры – без случайных событий монстры всегда будут атаковать вас одинаково, вы всегда найдете одно и то же сокровище, расположение подземелий никогда не изменится и т.д. И это не сделает игру очень хорошей.
Так как же нам генерировать случайные числа? В реальной жизни мы часто получаем случайные результаты, например, подбрасывая монету, бросая кости или тасуя колоду карт. Эти события включают в себя так много физических переменных (например, гравитацию, трение, сопротивление воздуха, импульс и т.д.), что их почти невозможно предсказать или контролировать, и они дают результаты, которые во всех смыслах случайны.
Однако компьютеры не предназначены для использования преимуществ физических переменных – ваш компьютер не может подбрасывать монету, бросать кости или тасовать реальные карты. Компьютеры живут в управляемом электрическом мире, где всё двоично (ложь или истина) и нет промежуточного состояния. По самой своей природе компьютеры предназначены для получения максимально предсказуемых результатов. Когда вы говорите компьютеру вычислить 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
с равномерным распределением чисел между ними.
Мы делаем это в пять этапов:
- Умножаем наш результат от
std::rand()
на дробьfraction
. Это преобразует результатrand()
в число с плавающей запятой от 0 (включительно) до 1 (не включая). - Если
rand()
возвращает 0, тогда0 * fraction
по-прежнему равно 0. Еслиrand()
возвращаетRAND_MAX
, тогдаRAND_MAX * fraction
равноRAND_MAX / (RAND_MAX + 1)
, что немного меньше 1. Любые другие числа, возвращаемые функциейrand()
будут равномерно распределены между этими двумя точками. - Затем нам нужно знать, сколько чисел мы можем вернуть. Другими словами, сколько чисел находится между
min
(включительно) иmax
(включительно)? - Это просто (
max - min + 1
). Например, еслиmax
= 8 иmin
= 5, (max
-min
+ 1) = (8 - 5 + 1) = 4. Между 5 и 8 есть 4 числа (то есть 5, 6, 7 и 8). - Умножаем два предыдущих результата вместе. Если у нас было число с плавающей запятой от 0 (включительно) до 1 (не включая), а затем мы умножили его на (
max - min + 1
), теперь у нас есть число с плавающей запятой между 0 (включительно) и (max - min + 1
) (не включая). - Преобразуем предыдущий результат в целое число. Это удаляет любую дробную составляющую, оставляя нам целочисленный результат от 0 (включительно) до (
max
-min
) (включительно). - Наконец, мы добавляем
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()
не работает, пока вы не начнете вызывать его чаще, чем один раз в секунду (поскольку начальное число изменяется только один раз в секунду). Не забудьте сделать генератор случайных чисел статическим или объявить его вне функции.