Как написать программу генератор случайных чисел

JavaScript, Node.JS


Рекомендация: подборка платных и бесплатных курсов таргетированной рекламе — https://katalog-kursov.ru/

Вы когда-нибудь задумывались, как работает Math.random()? Что такое случайное число и как оно получается? А представьте вопрос на собеседовании?—?напишите свой генератор случайных чисел в пару строк кода. И так, что же это такое, случайность и возможно ли ее предсказать?

Меня очень увлекают различные IT головоломки и задачки и генератор случайных чисел — одна из таких задачек. Обычно в своем телеграм канале я разбираю всякие головоломки и разные задачи с собеседований. Задача про генератор случайных чисел набрала большую популярность и мне захотелось увековечить ее в недрах одного из авторитетных источников информации — то бишь здесь, на Хабре.

Данный материал будет полезен всем тем фронтендерам и Node.js разработчикам, кто на острие технологий и хочет попасть в блокчейн проект/стартап, где вопросы про безопасность и криптографию, хотя бы на базовом уровне, спрашивают даже у фронтендеров.

Генератор псевдослучайных чисел и генератор случайных чисел

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

Этот источник используется для накопления энтропии с последующим получением из неё начального значения (initial value, seed), которое необходимо генераторам случайных чисел (ГСЧ) для формирования случайных чисел.

Генератор ПсевдоСлучайных Чисел использует единственное начальное значение, откуда и следует его псевдослучайность, в то время как Генератор Случайных Чисел всегда формирует случайное число, имея в начале высококачественную случайную величину, которая берется из различных источников энтропии.

Энтропия?—?это мера беспорядка. Информационная энтропия?—?мера неопределённости или непредсказуемости информации.

Выходит, что чтобы создать псевдослучайную последовательность нам нужен алгоритм, который будет генерить некоторую последовательность на основании определенной формулы. Но такую последовательность можно будет предсказать. Тем не менее, давайте пофантазируем, как бы могли написать свой генератор случайных чисел, если бы у нас не было Math.random()

ГПСЧ имеет некоторый алгоритм, который можно воспроизвести.
ГСЧ?—?это получение чисел полностью из какого либо шума, возможность просчитать который стремится к нулю. При этом в ГСЧ есть определенные алгоритмы для выравнивания распределения.

Придумываем свой алгоритм ГПСЧ

Генератор псевдослучайных чисел (ГПСЧ, англ. pseudorandom number generator, PRNG)?—?алгоритм, порождающий последовательность чисел, элементы которой почти независимы друг от друга и подчиняются заданному распределению (обычно равномерному).

Мы можем взять последовательность каких-то чисел и брать от них модуль числа. Самый простой пример, который приходит в голову. Нам нужно подумать, какую последовательность взять и модуль от чего. Если просто в лоб от 0 до N и модуль 2, то получится генератор 1 и 0:

function* rand() {
  const n = 100;
  const mod = 2;
  let i = 0;
  while (true) {
    yield i % mod;
    if (i++ > n) i = 0;
  }
}
let i = 0;
for (let x of rand()) {
 if (i++ > 100) break;
  console.log(x);
}

Эта функция генерит нам последовательность 01010101010101… и назвать ее даже псевдослучайной никак нельзя. Чтобы генератор был случайным, он должен проходить тест на следующий бит. Но у нас не стоит такой задачи. Тем не менее даже без всяких тестов мы можем предсказать следующую последовательность, значит такой алгоритм в лоб не подходит, но мы в нужном направлении.

А что если взять какую-то известную, но нелинейную последовательность, например число PI. А в качестве значения для модуля будем брать не 2, а что-то другое. Можно даже подумать на тему меняющегося значения модуля. Последовательность цифр в числе Pi считается случайной. Генератор может работать, используя числа Пи, начиная с какой-то неизвестной точки. Пример такого алгоритма, с последовательностью на базе PI и с изменяемым модулем:

const vector = [...Math.PI.toFixed(48).replace('.','')];
function* rand() {
 for (let i=3; i<1000; i++) {
   if (i > 99) i = 2;
    for (let n=0; n<vector.length; n++) {
      yield (vector[n] % i);
    }
  }
}

Но в JS число PI можно вывести только до 48 знака и не более. Поэтому предсказать такую последовательность все так же легко и каждый запуск такого генератора будет выдавать всегда одни и те же числа. Но наш генератор уже стал показывать числа от 0 до 9.

Мы получили генератор чисел от 0 до 9, но распределение очень неравномерное и каждый раз он будет генерировать одну и ту же последовательность.

Мы можем взять не число Pi, а время в числовом представлении и это число рассматривать как последовательность цифр, причем для того, чтобы каждый раз последовательность не повторялась, мы будем считывать ее с конца. Итого наш алгоритм нашего ГПСЧ будет выглядеть так:

function* rand() {
 let newNumVector = () => [...(+new Date)+''].reverse();
 let vector = newNumVector();
 let i=2;
 while (true) {
    if (i++ > 99) i = 2;
    let n=-1;
    while (++n < vector.length) yield (vector[n] % i);
    vector = newNumVector();
  }
}

// TEST:
let i = 0;
for (let x of rand()) {
  if (i++ > 100) break;
  console.log(x)
}

Вот это уже похоже на генератор псевдослучайных чисел. И тот же Math.random()?—?это ГПСЧ, про него мы поговорим чуть позже. При этом у нас каждый раз первое число получается разным.

Собственно на этих простых примерах можно понять как работают более сложные генераторы случайных числе. И есть даже готовые алгоритмы. Для примера разберем один из них?—?это Линейный конгруэнтный ГПСЧ(LCPRNG).

Линейный конгруэнтный ГПСЧ

Линейный конгруэнтный ГПСЧ(LCPRNG)?—?это распространённый метод для генерации псевдослучайных чисел. Он не обладает криптографической стойкостью. Этот метод заключается в вычислении членов линейной рекуррентной последовательности по модулю некоторого натурального числа m, задаваемой формулой. Получаемая последовательность зависит от выбора стартового числа?—?т.е. seed. При разных значениях seed получаются различные последовательности случайных чисел. Пример реализации такого алгоритма на JavaScript:

const a = 45;
const c = 21;
const m = 67;
var seed = 2;
const rand = () => seed = (a * seed + c) % m;
for(let i=0; i<30; i++)
  console.log( rand() )

Многие языки программирования используют LСPRNG (но не именно такой алгоритм(!)).

Как говорилось выше, такую последовательность можно предсказать. Так зачем нам ГПСЧ? Если говорить про безопасность, то ГПСЧ?—?это проблема. Если говорить про другие задачи, то эти свойства?—?могут сыграть в плюс. Например для различных спец эффектов и анимаций графики может понадобиться частый вызов random. И вот тут важны распределение значений и перформанс! Секурные алгоритмы не могут похвастать скоростью работы.

Еще одно свойство?—?воспроизводимость. Некоторые реализации позволяют задать seed, и это очень полезно, если последовательность должна повторяться. Воспроизведение нужно в тестах, например. И еще много других вещей существует, для которых не нужен безопасный ГСЧ.

Как устроен Math.random()

Метод Math.random() возвращает псевдослучайное число с плавающей запятой из диапазона [0, 1), то есть, от 0 (включительно) до 1 (но не включая 1), которое затем можно отмасштабировать до нужного диапазона. Реализация сама выбирает начальное зерно для алгоритма генерации случайных чисел; оно не может быть выбрано или сброшено пользователем.

Как устроен алгоритм Math.random()?—?интересный вопрос. До недавнего времени, а именно до 49 Chrome использовался алгоритм MWC1616:

uint32_t state0 = 1;
uint32_t state1 = 2;
uint32_t mwc1616() {
   state0 = 18030 * (state0 & 0xffff) + (state0 >> 16);
   state1 = 30903 * (state1 & 0xffff) + (state1 >> 16);
   return (state0 << 16) + (state1 & 0xffff);
}

Именно этот алгоритм генерит нам последовательность псевдослучайных чисел в промежутке между 0 и 1.

Предсказываем Math.random()

Чем это было чревато? Есть такой квест: alf.nu/ReturnTrue

В нем есть задача:

{
	let rand = Math.random();
	function random4(x) {
	    return rand === x;
	}
}

random4(???)

Что нужно вписать вместо вопросов, чтобы функция вернула true? Кажется что это невозможно. Но, это возможно, если вы заглядывали в спеку и видели алгоритм ГПСЧ V8. Решение этой задачи в свое время мне показал Роман Дворнов:

random4(function(){var A=18030,B=36969,F=65535,Z=16,M=Math,I=M.imul,c=M.random,M=M.pow(2,32),k,d,g=c()*M,h=c()*M;for(k=0;F^k&&(c=I(A,g>>>Z)+k++)&F^h>>>Z;);for(k=0;F^k&&(d=I(B,g&F)+k++)&F^h&F;);for(k=2;k—;g=c<<Z|d&F)c=c/A|c%A<<Z,d=d/B|d%B<<Z;return(g<0?g+M:g)/M}())

Этот код работал в 70% случаев для Chrome < 49 и Node.js < 5. Рома Дворнов, как всегда, показал чудеса магии, которая не что иное, как глубокое понимание внутренних механизмов браузеров. Я все жду, когда Роман сделает доклад на основе этих событий или напишет более подробную статью.

Что здесь происходит? Все дело в том, что алгоритм можно предсказать. Чтобы это было нагляднее, можно сгенерировать картинку случайных пикселей. На сайте есть такой генератор. Вот что было, когда в браузере был алгоритм MWC1616:

image

Видите эти равномерности на левом слайде? Изображение показывает проблему с распределением значений. На картинке слева видно, что значения местами сильно группируются, а местами выпадают большие фрагменты. Как следствие?—?числа можно предсказать.

Выходит что мы можем отреверсить Math.random() и предсказать, какое было загадано число на основе того, что получили в данный момент времени. Для этого получаем два значения через Math.random(). Затем вычисляем внутреннее состояние по этим значениям. Имея внутреннее состояние можем предсказывать следующие значения Math.random() при этом не меняя внутреннее состояние. Меняем код так так, чтобы вместо следующего возвращалось предыдущее значение. Собственно все это и описано в коде-решении для задачи random4. Но потом алгоритм изменили (подробности читайте в спеке). Его можно будет сломать, как только у нас в JS появится нормальная работа с 64 битными числами. Но это уже будет другая история.

Новый алгоритм выглядит так:


uint64_t state0 = 1;
uint64_t state1 = 2;
uint64_t xorshift128plus() {
   uint64_t s1 = state0;
   uint64_t s0 = state1;
   state0 = s0;
   s1 ^= s1 << 23;
   s1 ^= s1 >> 17;
   s1 ^= s0;
   s1 ^= s0 >> 26;
   state1 = s1;
   return state0 + state1;
}

Его все так же можно будет просчитать и предсказать. Но пока у нас нет “длинной математики” в JS. Можно попробовать через TypedArray сделать или использовать специальные библиотеки. Возможно кто-то однажды снова напишет предсказатель. Возможно это будешь ты, читатель. Кто знает ;)

Сrypto Random Values

Метод Math.random() не предоставляет криптографически стойкие случайные числа. Не используйте его ни для чего, связанного с безопасностью. Вместо него используйте Web Crypto API (API криптографии в вебе) и более точный метод window.crypto.getRandomValues().

Пример генерации случайного числа:

let [rvalue] = crypto.getRandomValues(new Uint8Array(1));
console.log( rvalue )

Но, в отличие от ГПСЧ Math.random(), этот метод очень ресурсоемкий. Дело в том, что данный генератор использует системные вызовы в ОС, чтобы получить доступ к источникам энтропии (мак адрес, цпу, температуре, etc…).

Добавлено 3 июня 2021 в 21:55

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

Так как же нам генерировать случайные числа? В реальной жизни мы часто получаем случайные результаты, например, подбрасывая монету, бросая кости или тасуя колоду карт. Эти события включают в себя так много физических переменных (например, гравитацию, трение, сопротивление воздуха, импульс и т.д.), что их почти невозможно предсказать или контролировать, и они дают результаты, которые во всех смыслах случайны.

Однако компьютеры не предназначены для использования преимуществ физических переменных – ваш компьютер не может подбрасывать монету, бросать кости или тасовать реальные карты. Компьютеры живут в управляемом электрическом мире, где всё двоично (ложь или истина) и нет промежуточного состояния. По самой своей природе компьютеры предназначены для получения максимально предсказуемых результатов. Когда вы говорите компьютеру вычислить 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 с равномерным распределением чисел между ними.

Мы делаем это в пять этапов:

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

Теги

C++ / CppLearnCppВихрь Мерсенна / Mersenne TwisterГенератор псевдослучайных чисел (ГПСЧ) / Pseudo-random number generator (PRNG)Для начинающихОбучениеПрограммирование

В статье вы узнаете, как использовать std.random и чем он хорош


Содержание

В 2011 году новый стандарт C++11 добавил в язык заголовок <random>, в котором описаны средства для работы со случайными числами на профессиональном уровне. Эти средства заменяют функцию rand и дают гораздо больше гибкости.

Но чтобы этим воспользоваться, надо немного разбираться в теории.

Почему в IT все числа неслучайные

Казалось бы, что мешает использовать в программах случайные числа? К сожалению, процессор на это не способен: его поведение строго детерминировано и не допускает никаких случайностей.

  • Для генерации по-настоящему случайных, ничем не связанных чисел операционной системе приходится использовать средства, недоступные обычным приложениям; такие случайные числа называются криптографически стойкими случайными числами
  • Генерация системой таких случайных чисел работает медленно, и при нехватке скорости система просто отдаёт приложениями псевдослучайные числа либо заставляет их ожидать, пока появится возможность вернуть случайное число

Вы можете узнать об этом подробнее, прочитав о разнице между устройствами /dev/random и /dev/urandom в ОС Linux

Pseudo-random Numbers Generator (PRNG)

Генератор псевдослучайных чисел (PRNG) — это алгоритм генерации последовательности чисел, похожих на случайные числа. Псевдослучайные числа не являются по-настоящему случайные, т.е. между ними остаются связывающие их закономерности.

Общий принцип генерации легко показать в примере:

#include <iostream>

int main()
{
    unsigned value = 18;
    // Порождаем и выводим 20 чисел, используя число 18 как зерно
    for (int i = 0; i < 20; ++i)
    {
        // Итеративно вычисляем новое значение value.
        value = (value * 73129 + 95121) % 100000;
        std::cout << value << std::endl;
    }
}

Несложная итеративная формула, содержащая умножение, сложение и деление по остатку на разные константы, создаёт целую серию чисел, похожих на случайные:

11443
10268
83693
13222
6759
74032
13953
64058
25307
70724
3221
43630
13391
65560
65065
66210
98915
82860
96765
55510

Очевидно, что между числами есть взаимосвязь: они вычислены по одной и той же формуле. Но для пользователя видимой взаимосвязи нет.

Время как источник случайности

Если вы запустите предыдущую программу несколько раз, вы обнаружите проблему: числа будут те же самые. Причина проста — в начале последовательности мы используем всегда одно и то же число, 18. Для последовательности это число является зерном (англ. seed), и чтобы последовательность менялась с каждым запуском, зерно должно быть случайным.

Простейший, но не самый лучший способ получения зерна: взять текущее календарное время в секундах. Для этой цели мы воспользуемся функцией std::time_t time(std::time_t* arg).

Функция std::time возвращает число, представляющее время, прошедшее с полуночи 1 января 1970 года. Обычно это время в секундах, также известное как UNIX Timestamp. Параметр arg можно игнорировать и передавать нулевой указатель (вы можете в этом убедиться, прочитав документацию).

#include <iostream>
#include <ctime>

int main()
{
    unsigned value = unsigned(std::time(nullptr));
    // Порождаем и выводим 20 чисел, используя время UNIX как зерно.
    for (int i = 0; i < 20; ++i)
    {
        value = (value * 73129 + 95121) % 100000;
        std::cout << value << std::endl;
    }
}

Теперь программа при каждом запуске будет выводить разные цепочки псевдослучайных чисел. При условии, что вы запускаете её не чаще одного раза в секунду.

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

Ограничение числа по диапазону

Ограничить числа по диапазону можно путём деления по остатку (что сократит длину входного диапазона) и добавления нижней границы диапазона:

#include <iostream>
#include <ctime>
#include <cassert>

// Структура генератора псевдослучайных чисел хранит одно число,
//  зерно следующего случайного числа.
struct PRNG
{
    unsigned seed = 0;
};

void initGenerator(PRNG& generator)
{
    // Получаем случайное зерно последовательности
    generator.seed = unsigned(std::time(nullptr));
}

// Генерирует число на отрезке [minValue, maxValue].
unsigned random(PRNG& generator, unsigned minValue, unsigned maxValue)
{
    // Проверяем корректность аргументов
    assert(minValue < maxValue);
    // Итеративно изменяем текущее число в генераторе
    generator.seed = (generator.seed * 73129 + 95121);
    
    // Приводим число к отрезку [minValue, maxValue]
    return (generator.seed % (maxValue + 1 - minValue)) + minValue;
}

int main()
{
    PRNG generator;
    initGenerator(generator);
    
    // Порождаем и выводим 10 чисел на отрезке [0, 7].
    std::cout << "ten numbers in range [0, 7]:" << std::endl;
    for (int i = 0; i < 10; ++i)
    {
        std::cout << random(generator, 0, 7) << std::endl;
    }
    
    // Порождаем и выводим 10 чисел на отрезке [10, 20].
    std::cout << "ten numbers in range [10, 20]:" << std::endl;
    for (int i = 0; i < 10; ++i)
    {
        std::cout << random(generator, 10, 20) << std::endl;
    }
}

Всё то же, только лучше: заголовок <random>

Заголовок random разделяет генерацию псевдослучайных чисел на 3 части и предоставляет три инструмента:

  • класс std::random_device, который запрашивает у операционной системы почти случайное целое число; этот класс более удачные зёрна, чем если брать текущее время
  • класс std::mt19937 и другие классы псевдо-случайных генераторов, задача которых — размножить одно зерно в целую последовательность чисел
  • класс std::uniform_int_distribution и другие классы распределений

Класс mt19937 реализует алгоритм размножения псевдослучайных чисел, известный как Вихрь Мерсенна. Этот алгоритм работает быстро и даёт хорошие результаты — гораздо более “случайные”, чем наш самописный метод, показанный ранее.

О распределениях скажем подробнее:

  • линейное распределение вероятностей (uniform distribution) возникает, когда вероятность появления каждого из допустимых чисел одинакова, т.е. каждое число может появиться с равным шансом
  • в некоторых прикладных задачах нужны другие распределения, в которых одни числа появляются чаще других — например, часто используется нормальное распределение (normal distribution)

В большинстве случаев вам подойдёт линейное распределение. Изредка пригодится нормальное, в котором вероятность появления числе тем ниже, чем дальше оно от среднего значения:

Иллюстрация

Теперь мы можем переписать

#include <iostream>
#include <random>
#include <cassert>

struct PRNG
{
    std::mt19937 engine;
};

void initGenerator(PRNG& generator)
{
    // Создаём псевдо-устройство для получения случайного зерна.
    std::random_device device;
    // Получаем случайное зерно последовательности
    generator.engine.seed(device());
}

// Генерирует целое число в диапазоне [minValue, maxValue)
unsigned random(PRNG& generator, unsigned minValue, unsigned maxValue)
{
    // Проверяем корректность аргументов
    assert(minValue < maxValue);
    
    // Создаём распределение
    std::uniform_int_distribution<unsigned> distribution(minValue, maxValue);
    
    // Вычисляем псевдослучайное число: вызовем распределение как функцию,
    //  передав генератор произвольных целых чисел как аргумент.
    return distribution(generator.engine);
}

// Генерирует число с плавающей точкой в диапазоне [minValue, maxValue)
float getRandomFloat(PRNG& generator, float minValue, float maxValue)
{
    // Проверяем корректность аргументов
    assert(minValue < maxValue);
    
    // Создаём распределение
    std::uniform_real_distribution<float> distribution(minValue, maxValue);
    
    // Вычисляем псевдослучайное число: вызовем распределение как функцию,
    //  передав генератор произвольных целых чисел как аргумент.
    return distribution(generator.engine);
}

int main()
{
    PRNG generator;
    initGenerator(generator);
    
    // Порождаем и выводим 10 чисел на отрезке [0, 7]
    std::cout << "ten integer numbers in range [0, 7]:" << std::endl;
    for (int i = 0; i < 10; ++i)
    {
        std::cout << random(generator, 0, 7) << std::endl;
    }
    
    // Порождаем и выводим 10 чисел на отрезке [10, 20]
    std::cout << "ten float numbers in range [10, 20]:" << std::endl;
    for (int i = 0; i < 10; ++i)
    {
        std::cout << getRandomFloat(generator, 10.f, 20.f) << std::endl;
    }
}

Особенность платформы: random_device в MinGW

Согласно стандарту C++, принцип работы std::random_device отдаётся на откуп разработчикам компилятора и стандартной библиотеки. В компиляторе G++ и его стандартной библиотеке libstdc++ класс random_device правильно работает на UNIX-платформах, но на Windows в некоторых дистрибутивах MinGW вместо случайных зёрен он возвращает одну и ту же константу!

В качестве обходного манёвра мы можем использовать текущее время в качестве зерна случайности. Для этого изменим функцию initGenerator:

#include <iostream>
#include <random>
#include <cassert>
#include <ctime>

struct PRNG
{
    std::mt19937 engine;
};

void initGenerator(PRNG& generator)
{
    // Используем время с 1 января 1970 года в секундах как случайное зерно
    const unsigned seed = unsigned(std::time(nullptr));
    generator.engine.seed(seed);
}

Приём №1: выбор случайного значения из предопределённого списка

Допусти, вы хотите случайно выбрать имя для кота. У вас есть список из 10 имён, которые подошли бы коту, но вы хотите реализовать случайный выбор. Достаточно случайно выбрать индекс в массиве имён! Такой же метод подошёл не только для генерации имени, но также для генерации цвета из заранее определённой палитры и для других задач.

Идея проиллюстрирована в коде

#include <iostream>
#include <vector>
#include <string>
#include <random>
#include <ctime>

struct PRNG
{
    std::mt19937 engine;
};

void initGenerator(PRNG& generator)
{
    // Используем время с 1 января 1970 года в секундах как случайное зерно
    const unsigned seed = unsigned(std::time(nullptr));
    generator.engine.seed(seed);
}

// Генерирует индекс в диапазоне [0, size)
size_t getRandomIndex(PRNG& generator, size_t size)
{
    // Создаём распределение
    std::uniform_int_distribution<size_t> distribution(0, size - 1);
    
    // Вычисляем псевдослучайное число: вызовем распределение как функцию,
    //  передав генератор произвольных целых чисел как аргумент.
    return distribution(generator.engine);
}

int main()
{
    std::vector<std::string> names = {
        "Barsik",
        "Murzik",
        "Pushok",
        "Amor",
        "Balu",
        "Vert",
        "Damar",
        "Kamelot",
        "Mavrik",
        "Napoleon"
    };
    
    PRNG generator;
    initGenerator(generator);
    
    // Порождаем и выводим 3 случайных имёни из заданного списка
    
    std::cout << "3 random cat names:" << std::endl;
    for (int i = 0; i < 3; ++i)
    {
        const size_t nameIndex = getRandomIndex(generator, names.size());
        std::cout << names[nameIndex] << std::endl;
    }
}

Приём №2: отбрасываем неподходящее значение

Если предыдущую программу запустить несколько раз, то рано или поздно у вас возникнет ситуация, когда одно и то же имя было выведено дважды. Что, если вам нужно уникальное значение, не выпадавшее прежде за время запуска программы?

Тогда используйте цикл, чтобы запрашивать случайные значения до тех пор, пока очередное значение не попадёт под ваши требования. Будьте аккуратны: если требования нереалистичные, вы получите бесконечный цикл!

Доработаем программу, добавив цикл while в функцию main. Для сохранения уже использованных имён воспользуемся структурой данных std::set из заголовка <set>, представляющей динамическое множество.

#include <iostream>
#include <vector>
#include <string>
#include <set>
#include <random>
#include <ctime>

struct PRNG
{
    std::mt19937 engine;
};

void initGenerator(PRNG& generator)
{
    // Используем время с 1 января 1970 года в секундах как случайное зерно
    const unsigned seed = unsigned(std::time(nullptr));
    generator.engine.seed(seed);
}

// Генерирует индекс в диапазоне [0, size)
size_t getRandomIndex(PRNG& generator, size_t size)
{
    // Создаём распределение
    std::uniform_int_distribution<size_t> distribution(0, size - 1);
    
    // Вычисляем псевдослучайное число: вызовем распределение как функцию,
    //  передав генератор произвольных целых чисел как аргумент.
    return distribution(generator.engine);
}

int main()
{
    std::vector<std::string> names = {
        "Barsik",
        "Murzik",
        "Pushok",
        "Amor",
        "Balu",
        "Vert",
        "Damar",
        "Kamelot",
        "Mavrik",
        "Napoleon"
    };
    
    PRNG generator;
    initGenerator(generator);
    
    // Множество, хранящее индексы использованных имён.
    std::set<size_t> usedIndexes;

    // Порождаем и выводим 3 случайных имёни из заданного списка
    std::cout << "3 random cat names:" << std::endl;
    for (int i = 0; i < 3; ++i)
    {
        size_t nameIndex = 0;
        while (true)
        {
            // Запрашиваем случайный индекс
            nameIndex = getRandomIndex(generator, names.size());
            // Проверяем, что индекс ранее не встречался
            if (usedIndexes.find(nameIndex) == usedIndexes.end())
            {
                // Если не встречался, добавляем в множество и выходим
                //  из цикла: уникальный индекс найден
                usedIndexes.insert(nameIndex);
                break;
            }
            else
            {
                // Отладочная печать отброшенного индекса
                std::cout << "discard index " << nameIndex << std::endl;
            }
        }
        std::cout << "index: " << nameIndex << std::endl;
        std::cout << names[nameIndex] << std::endl;
    }
}


Содержание

  • 1. Как в C++ сгенерировать случайное число? Функция rand(). Пример
  • 2. Функция srand(). Назначение. Пример
  • 3. Функция time(). Назначение. Сочетание функций rand()srand()time(). Пример
  • 4. Как сгенерировать случайное целое число в заданных пределах? Пример
  • 5. Заполнение двумерной матрицы случайными целыми числами в указанных пределах. Пример
  • 6. Как сгенерировать случайное число с плавающей запятой в указанных пределах? Пример
  • Связанные темы

Поиск на других ресурсах:

1. Как в C++ сгенерировать случайное число? Функция rand(). Пример

В языке C++ существуют средства для генерирования случайных чисел. Чтобы сгенерировать случайное число используется функция rand(), которая размещается в библиотечном файле stdlib.h. Синтаксис объявления функции следующий:

int rand();

Функция возвращает случайное целочисленное значение, которое лежит в пределах от 0 до 32767.

Пример.

#include <iostream>
#include <stdlib.h>
using namespace std;

void main()
{
  // Получить случайное число
  int x;
  x = rand();
  cout << "x = " << x << endl;

  // Получить еще одно случайное число
  int y;
  y = rand();
  cout << "y = " << y << endl;
}

Результат выполнения программы

x = 41
y = 18467

 

2. Функция srand(). Назначение. Пример

Если несколько раз запустить текст программы из п. 1, то будет получен один и тот же результат (одни и те же числа). Значит, сама по себе функция rand() генерирует одни и те же последовательности чисел. Чтобы получить разные последовательности чисел нужно объединить функцию rand() с функцией srand().

Функция srand() из библиотеки stdlib.h предназначена для установки начальной точки, из которой происходит генерирование случайных чисел. Синтаксис объявления функции следующий:

void srand(unsigned int startValue);

здесь startValue – целочисленное значение, которое служит отправной точкой для генерирования последовательности случайных чисел функцией rand(). Изменяя значение startValue, можно получать разные последовательности случайных чисел.

Пример.

#include <iostream>
#include <stdlib.h>
using namespace std;

void main()
{
  // Установить начальную точку генерирования последовательности
  srand(55);

  // Получить случайное число
  int x;
  x = rand();
  cout << "x = " << x << endl;

  // Получить еще одно случайное число
  int y;
  y = rand();
  cout << "y = " << y << endl;
}

Результат выполнения программы

x = 218
y = 9057

 

3. Функция time(). Назначение. Сочетание функций rand(), srand(), time(). Пример

Как видно из примера в п. 2, последовательность случайных чисел изменилась. Если в функции srand() вместо числа 55 установить другое число, то будет получена другая последовательность. Однако, текст программы статический и при многократном запуске программы это число будет неизменным. В результате будет получаться одна и та же последовательность случайных чисел. Во избежание этого недостатка, нужно чтобы стартовое значение в функции srand() постоянно изменялось.

Для того, чтобы в функции srand() получить разные начальные значения используется функция time() из библиотеки time.h.

Если функцию time() вызвать с параметром NULL, то эта функция возвратит количество миллисекунд, которые прошли с 1 января 1970 года. Значит, число миллисекунд будет зависеть от момента времени, в который пользователь запустил программу на выполнение. А этот момент каждый раз будет другим.

Если эти миллисекунды поместить в функцию srand() как показано ниже

srand(time(NULL));

то каждый раз при запуске программы будет создана новая отправная точка в генерировании последовательности чисел функцией rand(). И, как следствие, будут получаться разные последовательности случайных чисел.

Пример. В примере демонстрируется генерирование последовательности из двух случайных чисел. Каждый раз при запуске программы будет получена новая последовательность.

#include <iostream>
#include <stdlib.h>
#include <time.h>
using namespace std;

void main()
{
  // Установить начальную точку генерирования последовательности
  // использовать функцию time(NULL)
  srand(time(NULL));

  // Получить случайное число
  int x;
  x = rand();
  cout << "x = " << x << endl;

  // Получить еще одно случайное число
  int y;
  y = rand();
  cout << "y = " << y << endl;
}

Результат выполнения программы

x = 19040
y = 24635

 

4. Как сгенерировать случайное целое число в заданных пределах? Пример

В примере приведена функция GetRandomNumber(), которая генерирует случайное число в заданных пределах.

#include <iostream>
#include <stdlib.h> // нужен для вызова функций rand(), srand()
#include <time.h> // нужен для вызова функции time()
using namespace std;

// Функция генерирования случайного целочисленного числа в указанных пределах.
// Диапазон чисел: [min, max]
int GetRandomNumber(int min, int max)
{
  // Установить генератор случайных чисел
  srand(time(NULL));

  // Получить случайное число - формула
  int num = min + rand() % (max - min + 1);

  return num;
}

void main()
{
  // Использование функции GetRandomNumber()
  int number;
  number = GetRandomNumber(-10, 10); // Диапазон чисел: [-10, 10]
  cout << "number = " << number << endl;;
}

 

5. Заполнение двумерной матрицы случайными целыми числами в указанных пределах. Пример

Условие задачи. Дана двумерная матрица порядка n (n столбцов, n строк) целых чисел. Найти наибольшее из значений элементов, которые размещены в закрашенной части матрицы. Значение элементов матрицы формируются случайным образом и находятся в пределах [-5; +5].

Текст программы следующий

#include <iostream>
#include <stdlib.h> // нужен для вызова функции rand(), srand()
#include <time.h> // нужен для вызова функции time()
using namespace std;

void main()
{
  // Двумерные массивы.
  // Вычислить максимальный элемент нижней части матрицы

  // 1. Объявление переменных
  const int MAX_N = 10; // максимально-допустимая размерность матрицы
  const int MIN_VALUE = -5; // максимальное значение элементов матрицы
  const int MAX_VALUE = 5; // минимальное значение
  int A[MAX_N][MAX_N]; // исходная матрица
  int n; // текущий размер матрицы n*n
  int max; // результат - максимальное значение

  // 2. Ввод n
  cout << "n = ";
  cin >> n;

  // 3. Проверка n на корректность
  if ((n <= 1) || (n > MAX_N))
  {
    cout << "Wrong size of array." << endl;
    return;
  }

  // 4. Формирование матрицы A случайных чисел,
  // установить генератор случайных чисел
  srand(time(NULL));

  for (int i = 0; i < n; i++)
    for (int j = 0; j < n; j++)
    {
      // взять случайное число
      A[i][j] = MIN_VALUE + rand() % (MAX_VALUE - MIN_VALUE + 1);
    }

  // 5. Вывести массив A для проверки
  cout << endl << "Array A:" << endl;
  for (int i = 0; i < n; i++)
  {
    for (int j = 0; j < n; j++)
      cout << A[i][j] << 't';
    cout << endl;
  }

  // 6. Вычисление максимального значения
  bool f_first = true; // флажок первого элемента

  for (int i = 0; i < n; i++) // i - строки
    for (int j = 0; j < i; j++) // j - столбцы
      if (f_first)
      {
        // первый элемент принимается максимальным
        max = A[i][j];
        f_first = false;
      }
      else
      {
        if (max < A[i][j])
          max = A[i][j];
      }

  // 7. Вывод результата
  cout << endl << "max = " << max << endl;
}

 

6. Как сгенерировать случайное число с плавающей запятой в указанных пределах? Пример

В примере демонстрируется функция GetRandomNumberFloat(), которая генерирует случайное число с плавающей запятой в указанных пределах.

#include <iostream>
#include <stdlib.h> // нужен для вызова функции rand(), srand()
#include <time.h> // нужен для вызова функции time()
using namespace std;

// Функция, которая генерирует случайное число с плавающей запятой и указанной точностью
// Функция получает 3 параметра:
// - min - нижний предел;
// - max - верхний предел;
// - precision - точность, количество знаков после комы.
double GetRandomNumberFloat(double min, double max, int precision)
{
  // Установить стартовую точку
  srand(time(NULL));

  double value;

  // получить случайное число как целое число с порядком precision
  value = rand() % (int)pow(10, precision);

  // получить вещественное число
  value = min + (value / pow(10, precision)) * (max - min);

  return value;
}

void main()
{
  // Использование функции GetRandomNumberFloat()
  double number;

  srand(time(NULL));

  // Получить число в диапазоне [0; 2] с точностью 2 знака после запятой
  number = GetRandomNumberFloat(0, 2, 2);
  cout << "number = " << number << endl;
}

 


Связанные темы

  • Разработка класса Random генерирования случайных чисел

 


Given n numbers, each with some frequency of occurrence. Return a random number with probability proportional to its frequency of occurrence.

Example: 

Let following be the given numbers.
  arr[] = {10, 30, 20, 40}  

Let following be the frequencies of given numbers.
  freq[] = {1, 6, 2, 1}  

The output should be
  10 with probability 1/10
  30 with probability 6/10
  20 with probability 2/10
  40 with probability 1/10

It is quite clear that the simple random number generator won’t work here as it doesn’t keep track of the frequency of occurrence. 

We need to somehow transform the problem into a problem whose solution is known to us.

One simple method is to take an auxiliary array (say aux[]) and duplicate the numbers according to their frequency of occurrence. Generate a random number(say r) between 0 to Sum-1(including both), where Sum represents summation of frequency array (freq[] in above example). Return the random number aux[r] (Implementation of this method is left as an exercise to the readers).

The limitation of the above method discussed above is huge memory consumption when frequency of occurrence is high. If the input is 997, 8761 and 1, this method is clearly not efficient.

How can we reduce the memory consumption? Following is detailed algorithm that uses O(n) extra space where n is number of elements in input arrays.
1. Take an auxiliary array (say prefix[]) of size n. 
2. Populate it with prefix sum, such that prefix[i] represents sum of numbers from 0 to i. 
3. Generate a random number(say r) between 1 to Sum(including both), where Sum represents summation of input frequency array. 
4. Find index of Ceil of random number generated in step #3 in the prefix array. Let the index be indexc
5. Return the random number arr[indexc], where arr[] contains the input n numbers.
  Before we go to the implementation part, let us have quick look at the algorithm with an example: 
      arr[]: {10, 20, 30} 
      freq[]: {2, 3, 1} 
      Prefix[]: {2, 5, 6} 
  Since last entry in prefix is 6, all possible values of r are [1, 2, 3, 4, 5, 6] 
         1: Ceil is 2. Random number generated is 10. 
         2: Ceil is 2. Random number generated is 10. 
         3: Ceil is 5. Random number generated is 20. 
         4: Ceil is 5. Random number generated is 20. 
         5: Ceil is 5. Random number generated is 20. 
         6. Ceil is 6. Random number generated is 30. 
  In the above example 
      10 is generated with probability 2/6. 
      20 is generated with probability 3/6. 
      30 is generated with probability 1/6.

How does this work? 
Any number input[i] is generated as many times as its frequency of occurrence because there exists count of integers in range(prefix[i – 1], prefix[i]] is input[i]. Like in the above example 3 is generated thrice, as there exists 3 integers 3, 4 and 5 whose ceil is 5. 

C++

#include <bits/stdc++.h>

using namespace std;

int findCeil(int arr[], int r, int l, int h)

{

    int mid;

    while (l < h)

    {

        mid = l + ((h - l) >> 1);

        (r > arr[mid]) ? (l = mid + 1) : (h = mid);

    }

    return (arr[l] >= r) ? l : -1;

}

int myRand(int arr[], int freq[], int n)

{

    int prefix[n], i;

    prefix[0] = freq[0];

    for (i = 1; i < n; ++i)

        prefix[i] = prefix[i - 1] + freq[i];

    int r = (rand() % prefix[n - 1]) + 1;

    int indexc = findCeil(prefix, r, 0, n - 1);

    return arr[indexc];

}

int main()

{

    int arr[] = {1, 2, 3, 4};

    int freq[] = {10, 5, 20, 100};

    int i, n = sizeof(arr) / sizeof(arr[0]);

    srand(time(NULL));

    for (i = 0; i < 5; i++)

    cout << myRand(arr, freq, n) << endl;

    return 0;

}

C

#include <stdio.h>

#include <stdlib.h>

int findCeil(int arr[], int r, int l, int h)

{

    int mid;

    while (l < h)

    {

         mid = l + ((h - l) >> 1); 

        (r > arr[mid]) ? (l = mid + 1) : (h = mid);

    }

    return (arr[l] >= r) ? l : -1;

}

int myRand(int arr[], int freq[], int n)

{

    int prefix[n], i;

    prefix[0] = freq[0];

    for (i = 1; i < n; ++i)

        prefix[i] = prefix[i - 1] + freq[i];

    int r = (rand() % prefix[n - 1]) + 1;

    int indexc = findCeil(prefix, r, 0, n - 1);

    return arr[indexc];

}

int main()

{

    int arr[]  = {1, 2, 3, 4};

    int freq[] = {10, 5, 20, 100};

    int i, n = sizeof(arr) / sizeof(arr[0]);

    srand(time(NULL));

    for (i = 0; i < 5; i++)

      printf("%dn", myRand(arr, freq, n));

    return 0;

}

Java

class GFG

{

static int findCeil(int arr[], int r, int l, int h)

{

    int mid;

    while (l < h)

    {

        mid = l + ((h - l) >> 1);

        if(r > arr[mid])

            l = mid + 1;

        else

            h = mid;

    }

    return (arr[l] >= r) ? l : -1;

}

static int myRand(int arr[], int freq[], int n)

{

    int prefix[] = new int[n], i;

    prefix[0] = freq[0];

    for (i = 1; i < n; ++i)

        prefix[i] = prefix[i - 1] + freq[i];

    int r = ((int)(Math.random()*(323567)) % prefix[n - 1]) + 1;

    int indexc = findCeil(prefix, r, 0, n - 1);

    return arr[indexc];

}

public static void main(String args[])

{

    int arr[] = {1, 2, 3, 4};

    int freq[] = {10, 5, 20, 100};

    int i, n = arr.length;

    for (i = 0; i < 5; i++)

    System.out.println( myRand(arr, freq, n) );

}

}

Python3

import random

def findCeil(arr, r, l, h) :

    while (l < h) :   

        mid = l + ((h - l) >> 1);

        if r > arr[mid] :

            l = mid + 1

        else :

            h = mid

    if arr[l] >= r :

        return l

    else :

        return -1

def myRand(arr, freq, n) :

    prefix = [0] * n

    prefix[0] = freq[0]

    for i in range(n) :

        prefix[i] = prefix[i - 1] + freq[i]

    r = random.randint(0, prefix[n - 1]) + 1

    indexc = findCeil(prefix, r, 0, n - 1)

    return arr[indexc]

arr = [1, 2, 3, 4]

freq = [10, 5, 20, 100]

n = len(arr)

for i in range(5) :

    print(myRand(arr, freq, n))

C#

using System;

class GFG{

static int findCeil(int[] arr, int r,

                    int l, int h) 

    int mid;

    while (l < h) 

    

        mid = l + ((h - l) >> 1);

        if (r > arr[mid]) 

            l = mid + 1;

        else

            h = mid; 

    

    return (arr[l] >= r) ? l : -1; 

}

static int myRand(int[] arr, int[] freq) 

    int n = arr.Length;

    int[] prefix = new int[n];

    int i; 

    prefix[0] = freq[0]; 

    for(i = 1; i < n; ++i) 

        prefix[i] = prefix[i - 1] + freq[i]; 

    Random rand = new Random();

    int r = rand.Next(prefix[n - 1] + 1; 

    int indexc = findCeil(prefix, r, 0, n - 1); 

    return arr[indexc]; 

}

static void Main()

{

    int[] arr = { 1, 2, 3, 4 }; 

    int[] freq = { 10, 5, 20, 100 };

    for(int i = 0; i < 5; i++) 

        Console.WriteLine(myRand(arr, freq)); 

}

}

Javascript

<script>

    function findCeil(arr, r, l, h)

    {

        let mid;

        while (l < h)

        {

            mid = l + ((h - l) >> 1);

            (r > arr[mid]) ? (l = mid + 1) : (h = mid);

        }

        return (arr[l] >= r) ? l : -1;

    }

    function myRand(arr, freq,  n) {

        let prefix= [];

        let i;

        prefix[0] = freq[0];

        for (i = 1; i < n; ++i)

            prefix[i] = prefix[i - 1] + freq[i];

        let r = Math.floor((Math.random()* prefix[n - 1])) + 1;

        let indexc = findCeil(prefix, r, 0, n - 1);

        return arr[indexc];

    }

    let arr = [1, 2, 3, 4];

    let freq = [10, 5, 20, 100];

    let i;

    let n = arr.length;

    for (i = 0; i < 5; i++)

      document.write(myRand(arr, freq, n));

</script>

Output: May be different for different runs 

4
3
4
4
4

Time Complexity: O(n)
Auxiliary Space: O(n) because extra space for the array has been used

This article is compiled by Aashish Barnwal. Please write comments if you find anything incorrect, or you want to share more information about the topic discussed above.

Given n numbers, each with some frequency of occurrence. Return a random number with probability proportional to its frequency of occurrence.

Example: 

Let following be the given numbers.
  arr[] = {10, 30, 20, 40}  

Let following be the frequencies of given numbers.
  freq[] = {1, 6, 2, 1}  

The output should be
  10 with probability 1/10
  30 with probability 6/10
  20 with probability 2/10
  40 with probability 1/10

It is quite clear that the simple random number generator won’t work here as it doesn’t keep track of the frequency of occurrence. 

We need to somehow transform the problem into a problem whose solution is known to us.

One simple method is to take an auxiliary array (say aux[]) and duplicate the numbers according to their frequency of occurrence. Generate a random number(say r) between 0 to Sum-1(including both), where Sum represents summation of frequency array (freq[] in above example). Return the random number aux[r] (Implementation of this method is left as an exercise to the readers).

The limitation of the above method discussed above is huge memory consumption when frequency of occurrence is high. If the input is 997, 8761 and 1, this method is clearly not efficient.

How can we reduce the memory consumption? Following is detailed algorithm that uses O(n) extra space where n is number of elements in input arrays.
1. Take an auxiliary array (say prefix[]) of size n. 
2. Populate it with prefix sum, such that prefix[i] represents sum of numbers from 0 to i. 
3. Generate a random number(say r) between 1 to Sum(including both), where Sum represents summation of input frequency array. 
4. Find index of Ceil of random number generated in step #3 in the prefix array. Let the index be indexc
5. Return the random number arr[indexc], where arr[] contains the input n numbers.
  Before we go to the implementation part, let us have quick look at the algorithm with an example: 
      arr[]: {10, 20, 30} 
      freq[]: {2, 3, 1} 
      Prefix[]: {2, 5, 6} 
  Since last entry in prefix is 6, all possible values of r are [1, 2, 3, 4, 5, 6] 
         1: Ceil is 2. Random number generated is 10. 
         2: Ceil is 2. Random number generated is 10. 
         3: Ceil is 5. Random number generated is 20. 
         4: Ceil is 5. Random number generated is 20. 
         5: Ceil is 5. Random number generated is 20. 
         6. Ceil is 6. Random number generated is 30. 
  In the above example 
      10 is generated with probability 2/6. 
      20 is generated with probability 3/6. 
      30 is generated with probability 1/6.

How does this work? 
Any number input[i] is generated as many times as its frequency of occurrence because there exists count of integers in range(prefix[i – 1], prefix[i]] is input[i]. Like in the above example 3 is generated thrice, as there exists 3 integers 3, 4 and 5 whose ceil is 5. 

C++

#include <bits/stdc++.h>

using namespace std;

int findCeil(int arr[], int r, int l, int h)

{

    int mid;

    while (l < h)

    {

        mid = l + ((h - l) >> 1);

        (r > arr[mid]) ? (l = mid + 1) : (h = mid);

    }

    return (arr[l] >= r) ? l : -1;

}

int myRand(int arr[], int freq[], int n)

{

    int prefix[n], i;

    prefix[0] = freq[0];

    for (i = 1; i < n; ++i)

        prefix[i] = prefix[i - 1] + freq[i];

    int r = (rand() % prefix[n - 1]) + 1;

    int indexc = findCeil(prefix, r, 0, n - 1);

    return arr[indexc];

}

int main()

{

    int arr[] = {1, 2, 3, 4};

    int freq[] = {10, 5, 20, 100};

    int i, n = sizeof(arr) / sizeof(arr[0]);

    srand(time(NULL));

    for (i = 0; i < 5; i++)

    cout << myRand(arr, freq, n) << endl;

    return 0;

}

C

#include <stdio.h>

#include <stdlib.h>

int findCeil(int arr[], int r, int l, int h)

{

    int mid;

    while (l < h)

    {

         mid = l + ((h - l) >> 1); 

        (r > arr[mid]) ? (l = mid + 1) : (h = mid);

    }

    return (arr[l] >= r) ? l : -1;

}

int myRand(int arr[], int freq[], int n)

{

    int prefix[n], i;

    prefix[0] = freq[0];

    for (i = 1; i < n; ++i)

        prefix[i] = prefix[i - 1] + freq[i];

    int r = (rand() % prefix[n - 1]) + 1;

    int indexc = findCeil(prefix, r, 0, n - 1);

    return arr[indexc];

}

int main()

{

    int arr[]  = {1, 2, 3, 4};

    int freq[] = {10, 5, 20, 100};

    int i, n = sizeof(arr) / sizeof(arr[0]);

    srand(time(NULL));

    for (i = 0; i < 5; i++)

      printf("%dn", myRand(arr, freq, n));

    return 0;

}

Java

class GFG

{

static int findCeil(int arr[], int r, int l, int h)

{

    int mid;

    while (l < h)

    {

        mid = l + ((h - l) >> 1);

        if(r > arr[mid])

            l = mid + 1;

        else

            h = mid;

    }

    return (arr[l] >= r) ? l : -1;

}

static int myRand(int arr[], int freq[], int n)

{

    int prefix[] = new int[n], i;

    prefix[0] = freq[0];

    for (i = 1; i < n; ++i)

        prefix[i] = prefix[i - 1] + freq[i];

    int r = ((int)(Math.random()*(323567)) % prefix[n - 1]) + 1;

    int indexc = findCeil(prefix, r, 0, n - 1);

    return arr[indexc];

}

public static void main(String args[])

{

    int arr[] = {1, 2, 3, 4};

    int freq[] = {10, 5, 20, 100};

    int i, n = arr.length;

    for (i = 0; i < 5; i++)

    System.out.println( myRand(arr, freq, n) );

}

}

Python3

import random

def findCeil(arr, r, l, h) :

    while (l < h) :   

        mid = l + ((h - l) >> 1);

        if r > arr[mid] :

            l = mid + 1

        else :

            h = mid

    if arr[l] >= r :

        return l

    else :

        return -1

def myRand(arr, freq, n) :

    prefix = [0] * n

    prefix[0] = freq[0]

    for i in range(n) :

        prefix[i] = prefix[i - 1] + freq[i]

    r = random.randint(0, prefix[n - 1]) + 1

    indexc = findCeil(prefix, r, 0, n - 1)

    return arr[indexc]

arr = [1, 2, 3, 4]

freq = [10, 5, 20, 100]

n = len(arr)

for i in range(5) :

    print(myRand(arr, freq, n))

C#

using System;

class GFG{

static int findCeil(int[] arr, int r,

                    int l, int h) 

    int mid;

    while (l < h) 

    

        mid = l + ((h - l) >> 1);

        if (r > arr[mid]) 

            l = mid + 1;

        else

            h = mid; 

    

    return (arr[l] >= r) ? l : -1; 

}

static int myRand(int[] arr, int[] freq) 

    int n = arr.Length;

    int[] prefix = new int[n];

    int i; 

    prefix[0] = freq[0]; 

    for(i = 1; i < n; ++i) 

        prefix[i] = prefix[i - 1] + freq[i]; 

    Random rand = new Random();

    int r = rand.Next(prefix[n - 1] + 1; 

    int indexc = findCeil(prefix, r, 0, n - 1); 

    return arr[indexc]; 

}

static void Main()

{

    int[] arr = { 1, 2, 3, 4 }; 

    int[] freq = { 10, 5, 20, 100 };

    for(int i = 0; i < 5; i++) 

        Console.WriteLine(myRand(arr, freq)); 

}

}

Javascript

<script>

    function findCeil(arr, r, l, h)

    {

        let mid;

        while (l < h)

        {

            mid = l + ((h - l) >> 1);

            (r > arr[mid]) ? (l = mid + 1) : (h = mid);

        }

        return (arr[l] >= r) ? l : -1;

    }

    function myRand(arr, freq,  n) {

        let prefix= [];

        let i;

        prefix[0] = freq[0];

        for (i = 1; i < n; ++i)

            prefix[i] = prefix[i - 1] + freq[i];

        let r = Math.floor((Math.random()* prefix[n - 1])) + 1;

        let indexc = findCeil(prefix, r, 0, n - 1);

        return arr[indexc];

    }

    let arr = [1, 2, 3, 4];

    let freq = [10, 5, 20, 100];

    let i;

    let n = arr.length;

    for (i = 0; i < 5; i++)

      document.write(myRand(arr, freq, n));

</script>

Output: May be different for different runs 

4
3
4
4
4

Time Complexity: O(n)
Auxiliary Space: O(n) because extra space for the array has been used

This article is compiled by Aashish Barnwal. Please write comments if you find anything incorrect, or you want to share more information about the topic discussed above.

Понравилась статья? Поделить с друзьями:
  • Как написать программу вычисления площади круга
  • Как написать программу вычисления наименьшего общего кратного
  • Как написать программу воспитания доу
  • Как написать программу вирус
  • Как написать программу браузер