📚 Hub Books: Онлайн-чтение книгРазная литератураC++17 STL Стандартная библиотека шаблонов - Яцек Галовиц

C++17 STL Стандартная библиотека шаблонов - Яцек Галовиц

Шрифт:

-
+

Интервал:

-
+
1 ... 92 93 94 95 96 97 98 99 100 ... 121
Перейти на страницу:
представить результат броска. Для выполнения таких простых задач не нужно использовать библиотеку.

А если мы хотим смоделировать событие, которое случается с вероятностью 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 <<

1 ... 92 93 94 95 96 97 98 99 100 ... 121
Перейти на страницу:

Комментарии

Обратите внимание, что комментарий должен быть не короче 20 символов. Покажите уважение к себе и другим пользователям!

Никто еще не прокомментировал. Хотите быть первым, кто выскажется?