C++17 STL Стандартная библиотека шаблонов - Яцек Галовиц
Шрифт:
Интервал:
Закладка:
А если мы хотим смоделировать событие, которое случается с вероятностью 66%? Окей, можно создать формулу наподобие bool yesno = (rand()%100>66). (Погодите, нам следует использовать оператор >= или правильнее будет оставить оператор >?)
Кроме того, как смоделировать бросок нечестного кубика, грани которого могут выпасть с разной вероятностью? Как смоделировать более сложные распределения? Такие задачи могут быстро перерасти в научные. Чтобы сконцентрироваться на наших основных задачах, взглянем на инструменты, предоставляемые STL.
Библиотека содержит более дюжины алгоритмов распределения, которые могут формировать случайные числа для определенных потребностей. В этом примере мы очень кратко рассмотрим их все, а также более детально взглянем на самые полезные.
Как это делается
В этом примере мы будем генерировать случайные числа, придавать им форму и выводить на экран шаблоны распределения. Таким образом, рассмотрим их все и разберем их самые важные свойства, которые могут оказаться полезными, если потребуется смоделировать что-то конкретное.
1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространства имен std:
#include <iostream>
#include <iomanip>
#include <random>
#include <map>
#include <string>
#include <algorithm>
using namespace std;
2. Для каждого распределения, предоставляемого STL, выведем гистограмму, чтобы увидеть его характеристики, поскольку каждое из них выглядит особенным образом. Гистограмма принимает в качестве аргумента распределение и количество образцов, которые будут взяты из него. Затем создадим генератор случайных чисел по умолчанию и ассоциативный массив. В последнем будут соотнесены значения, полученные из распределения, со счетчиками, показывающими, как часто встречается то или иное значение. Мы всегда создаем экземпляр генератора случайных чисел, потому что все распределения используются только в качестве функции для формирования случайных чисел, которые все еще должны быть сгенерированы.
template <typename T>
void print_distro(T distro, size_t samples)
{
default_random_engine e;
map<int, size_t> 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]
<< " <samples>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_distributionn";
print_distro(uniform_int_distribution<int>{0, 9}, samples);
cout << "normal_distributionn";
print_distro(normal_distribution<double>{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<double> intervals {0, 5, 10, 30};
initializer_list<double> weights {0.2, 0.3, 0.5};
cout << "piecewise_constant_distributionn";
print_distro(
piecewise_constant_distribution<double>{
begin(intervals), end(intervals),
begin(weights)},
samples);
10. Распределение piecewise_linear_distribution создается аналогично, но веса работают совершенно по-другому. Для каждой граничной точки интервала существует одно значение веса. При переходе от одной границы к другой вероятность интерполируется линейно. Воспользуемся теми же интервалами, но передадим другой список весов:
cout << "piecewise_linear_distributionn";
initializer_list<double> weights2 {0, 1, 1, 0};
print_distro(
piecewise_linear_distribution<double>{
begin(intervals), end(intervals), begin(weights2)},
samples);
11. Распределение Бернулли — это еще одно важное распределение, поскольку распределяет лишь значения «да/нет», «попадание/промах» или «орел/решка» с конкретной вероятностью. Его выходными значениями будут только 0 и 1. Еще одним интересным распределением, полезным во многих случаях, является discrete_distribution. В нашем случае инициализируем его дискретными значениями 1, 2, 4, 8. Они интерпретируются как веса для возможных выходных значений от 0 до 3.
cout <<