C++17 STL Стандартная библиотека шаблонов — страница 94 из 119

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


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