В этом примере мы будем генерировать случайные числа, придавать им форму и выводить на экран шаблоны распределения. Таким образом, рассмотрим их все и разберем их самые важные свойства, которые могут оказаться полезными, если потребуется смоделировать что-то конкретное.
1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространства имен
std
:
#include
#include
#include
#include
#include
#include
using namespace std;
2. Для каждого распределения, предоставляемого STL, выведем гистограмму, чтобы увидеть его характеристики, поскольку каждое из них выглядит особенным образом. Гистограмма принимает в качестве аргумента распределение и количество образцов, которые будут взяты из него. Затем создадим генератор случайных чисел по умолчанию и ассоциативный массив. В последнем будут соотнесены значения, полученные из распределения, со счетчиками, показывающими, как часто встречается то или иное значение. Мы всегда создаем экземпляр генератора случайных чисел, потому что все распределения используются только в качестве функции для формирования случайных чисел, которые все еще должны быть сгенерированы.
template
void print_distro(T distro, size_t samples)
{
default_random_engine e;
map m;
3. Возьмем столько образцов, сколько указано в переменной
samples
, и заполним ими ассоциативный массив счетчиков. Таким образом получим очередную гистограмму. Простой вызов e()
даст необработанное простое число, distro(e)
придает случайным числам форму с помощью объекта распределения:
for (size_t i {0}; i < samples; ++i) {
m[distro(e)] += 1;
}
4. Чтобы получить выходные данные, которые помещаются в окно консоли, нужно узнать самое большое значение счетчика. Функция
max_element
поможет определить такое значение путем сравнения всех связанных счетчиков в массиве и возвращения итератора, указывающего на узел, содержащий данное значение. Зная это значение, можем определить, на какое число следует разделить все значения счетчиков, чтобы уместить полученный результат в окно консоли.
size_t max_elm (max_element(begin(m), end(m),
[](const auto &a, const auto &b) {
return a.second < b.second;
})->second);
size_t max_div (max(max_elm / 100, size_t(1)));
5. Теперь пройдем по массиву в цикле и выведем полоски из символов
'*'
для всех счетчиков большого размера. Остальные значения отбросим, поскольку некоторые генераторы случайных чисел распределяют числа так широко, что это переполнит наши окна консоли.
for (const auto [randval, count] : m) {
if (count < max_elm / 200) { continue; }
cout << setw(3) << randval << " : "
<< string(count / max_div, '*') << '\n';
}
}
6. В функции
main
проверим, предоставил ли пользователь ровно один параметр, который указывает, сколько именно образцов нужно взять из каждого распределения. Если пользователь передал ноль или несколько параметров, то сгенерируем ошибку:
int main(int argc, char **argv)
{
if (argc != 2) {
cout << "Usage: " << argv[0]
<< " \n"; return 1;
}
7. Теперь преобразуем аргумент командной строки в число с помощью вызова
std::stoull
:
size_t samples {stoull(argv[1])};
8. Сначала попробуем распределения
uniform_int_distribution
и normal_distribution
. Они используются в большинстве случаев, когда нужно применить генератор случайных чисел. Все, кто когда-то изучал стохастику в университете, скорее всего, слышали о них. Равномерное распределение принимает два значения, указывая нижнюю и верхнюю границы диапазона, в котором будут распределены случайные значения. Выбрав 0
и 9
, мы получим одинаково часто встречающиеся значения между 0
и 9
(включительно). Нормальное распределение принимает в качестве аргументов математическое ожидание и среднеквадратическое отклонение.
cout << "uniform_int_distribution\n";
print_distro(uniform_int_distribution{0, 9}, samples);
cout << "normal_distribution\n";
print_distro(normal_distribution{0.0, 2.0}, samples);
9. Еще одним очень интересным распределением является
piecewise_constant_distribution
. Оно принимает в качестве аргументов два входных диапазона. Первый диапазон содержит числа, которые указывают границы интервалов. Определив их как 0
, 5
, 10
, 30
, получим три интервала, простирающиеся от 0
до 4
, от 5
до 9
и от 10
до 29
. Еще один входной диапазон определяет веса входных диапазонов. Установив значения этих весов равными 0.2
, 0.3
, 0.5
, мы укажем, что из соответствующих интервалов случайные числа будут получены с вероятностями 20, 30 и 50%. Внутри каждого из интервалов значения будут иметь одинаковую вероятность выпадения.
initializer_list intervals {0, 5, 10, 30};
initializer_list weights {0.2, 0.3, 0.5};
cout << "piecewise_constant_distribution\n";
print_distro(
piecewise_constant_distribution{
begin(intervals), end(intervals),
begin(weights)},
samples);
10. Распределение
piecewise_linear_distribution
создается аналогично, но веса работают совершенно по-другому. Для каждой граничной точки интервала существует одно значение веса. При переходе от одной границы к другой вероятность интерполируется линейно. Воспользуемся теми же интервалами, но передадим другой список весов:
cout << "piecewise_linear_distribution\n";
initializer_list weights2 {0, 1, 1, 0};
print_distro(
piecewise_linear_distribution{
begin(intervals), end(intervals), begin(weights2)},
samples);
11. Распределение Бернулли — это еще одно важное распределение, поскольку распределяет лишь значения «да/нет», «попадание/промах» или «орел/решка» с конкретной вероятностью. Его выходными значениями будут только
0
и 1
. Еще одним интересным распределением, полезным во многих случаях, является discrete_distribution
. В нашем случае инициализируем его дискретными значениями 1
, 2
, 4
, 8
. Они интерпретируются как веса для возможных выходных значений от 0
до 3
.
cout << "bernoulli_distribution\n";
print_distro(std::bernoulli_distribution{0.75}, samples);
cout << "discrete_distribution\n";
print_distro(discrete_distribution{{1, 2, 4, 8}}, samples);
12. Существует множество других генераторов распределений. Они полезны только в очень специфических ситуациях. Если вы никогда о них не слышали, то они, возможно, вам и не нужны. Однако, поскольку наша программа создает «аккуратные» гистограммы, показывающие распределение, из интереса выведем их все:
cout << "binomial_distribution\n";
print_distro(binomial_distribution