Генерация случайных чисел
Дата создания: 2009-05-03 15:27:38
Последний раз редактировалось: 2012-02-08 06:53:25
Очень часто при разработке программ возникает необходимость в последовательности случайных чисел. В играх, последовательность случайных чисел может использоваться в разнообразнейших ситуациях: создание монстров, генерация территории, поведение искусственного интеллекта.
Многие вещи можно сделать и без генератора случайных чисел. Вот например, такая последовательность чисел не может считаться случайной: {0, 1, 2, 3, 4, 5, 6, 7}. Здесь, зная предыдущее число, очень легко угадать следующее. Существуют другие последовательности. Например: {0, 1, 3, 2, 6, 7, 5, 4}. Здесь, угадать следующее число значительно сложнее. Подобные последовательности иногда удобно использовать в программах. Данная последовательность определяется кодом Грея[1]. При первом осмотре кажется что тут представлены случайные числа.
Вместо использования в программах последовательности чисел, подчиняющихся какому-либо закону, во многих случаях лучше использовать случайно сгенерированные.
Генерация псевдослучайных чисел
Обычно используется генерация псевдослучайных чисел. Т.е. числа не совсем случайные. Мы уже рассматривали функцию rand(). Если Вы напишите программу использующую данную функцию, то при каждом запуске программы, rand() будет генерировать одну и ту же последовательность чисел. Последовательность сгенерированная rand() определяется начальным числом (seed). Сначала задаётся начальное число, затем, по определённой формуле вичисляются все остальные числа последовательности. Зная начальное число и формулу по которой рассчитываются числа, можно вычислить следующее число.
Такие алгоритмы называются детерминистическими (предопределёнными). Т.е. в них последовательность чисел создаётся на основе начальных данных. При этом последовательность чисел - псевдослучайная: т.е. зная начальное число, можно узнать и все следующие. Но пользователь навряд ли их узнает, числа будут казаться случайными.
Одним из предопределённых (детерминистических) алгоритмов создания случайных чисел является линейно-конгруэнтный.
Установка начального значения (для функции rand(), которую мы использовали до сих пор) выглядит примерно так:
int i; cin >> i; srand(i); // установка начального значения rand();
Функция srnd устанавливает начальное значение i.
Линейный конгруэнтный (linear congruential) метод генерации случайных чисел
Существует много методов генерации случайных чисел. Линейно-конгруэнтный лишь один из них. Метод довольно старый - 1950х годов. Разработал его Деррик Лемер.
Для реализации алгоритма необходиом задать четыре параметра:
Диапазон значений m, при этом m > 0.
Множитель a (0 <= a <= m).
Инкрементирующее значение c (0 <= c <= m).
Начальное значение X0 (0 <= X0 < m).
Определив эти параметры, можно воспользоваться формулой:
Xi+1 = (aXi + c) % m (где i больше или равно 0)
i - номер элемента в последовательности.
m - количество значений из которых формируется последовательность.
Напоминаю что % - остаток от деления
Давайте рассмотрим пример небольшой последовательности. Возьмите листок бумаги, карандаш и просчитайте все значения:
imax = 5; // формируем последовательность из 5 элементов
m = 10; // значения в последовательность варьируются от нуля до 9
a = 2;
c = 3;
X0 = 6; // начальное значение (seed)
Xi+1 = (2*Xi + 3) % 10 (где i больше или равно 0)
X1 = 15 % 10 = 5
X2 = 13 % 10 = 3
X3 = 9 % 10 = 9
X4 = 21 % 10 = 1
X5 = 5 % 10 = 5
Если мы увеличим последовательность, то значения начнут повторяться. Таким образом несколько неповторяющихся значений в последовательности формируют период. Период в данном примере: { 5, 3, 9, 1 }.
Итак, с помощью линейного конгруэнтного метода мы получили последовательность случайных чисел из пяти элементов.
Чтобы предугадать значения элементов в последовательности можно воспользоваться формулой:
Xi+k = (a*k*Xi + (a*k-1)*c/b) % m (где k и i больше или равны 0)
Здесь b = a - 1. При этом a >= 2, b >= 1.
Вичислим шестой элемент с помощью этой формулы (мы ведь уже знаем пятый):
X5+1 = (a*1*X5 + (a*1-1)*c/b) % m = (2*1*5 + (2*1-1)*3/1) % 10 = 13 % 10 = 3
Всё росто, правда?
У последовательности созданной с помощью линейного конгруэнтного метода и определённой целыми параметрами m, a, c и X0, период равен числу m когда выполняются следующие условия:
1.Наибольший общий делитель c и m равен 1.
2.b - кратно любому простому числу, являющемуся делителем m.
3.Если m кратно 4, тогда b также кратно 4.
Выбор m
Период не может быть больше числа m. Следовательно m должно быть довольно большим.
Лучше всего если бы m был равен максимальному элементу в последовательности imax+1 (единицу добавляем, так как отсчёт i идёт от нуля). Ещё одним популярным выбором является степень двойки 2n. При условии, что n - длина машинного слова в битах, от операции взятия остатка можно избавиться. Если m является степенью двойки, То операцию взятия остатка можно заменить на более быструю операцию поразрядного И[2] (хотя для современных компьютеров это не актуально):
x % 2n == x & (2n - 1)
Примеры (x - целое число):
x % 2 == x & 1; x % 4 == x & 3; x % 8 == x & 7;
Очень часто для m выбирают одно из простых чисел Мерсенна[3]. Часто число 231 - 1 используется, когда вычисления ведутся с 32-ух битными данными.
Множитель a
Множитель нужно выбирать так, чтобы период был как можно длиннее. Существует серьёзная проблема с данным коэффициентом: при малых значениях a, если текущий элемент последовательности достаточно мал, вполне вероятно, что следующий элемент тоже будет маленьким.
Инкремент c
Данный параметр можно выбирать довольно произвольно. Очень часто его задают в виде нуля, но при этом уменьшается длина периода и X0 != 0.
Начальное значение (seed)
Итак, мы выяснили, что в последовательности есть период. В нашем примере период равен всего четырём элементам. В других случаях период может быть очень большим, но он всегда есть! То есть при генерации случайных чисел линейным конгруэнтным методом если нам нужна очень большая последовательность, то в ней значения будут повторяться с определённой периодичностью.
Вот так вот! Генерация последовательных чисел - это самая настоящая мистификация!
Т.е. у нас допустим есть генератор случайных чисел. Мы его используем для разных программ. Всегда, всегда этот генератор создаёт одинаковую последовательность чисел (при одинаковых начальных значениях). Мы можем только установить начальное значение.
Вообще говоря, с помощью арифметических методов нельзя построить по настоящему случайную последовательность чисел. Как невесело шутил наш друг - Джон Фон Нейман:
"Anyone who uses arithmetic methods to produce random numbers is in a state of sin." (C) Джон Фон Нейман.
Грустно всё это. Вот так живёшь-живёшь. Находясь в состоянии неведения, думаешь: "А вот круто бы было создать генератор случайных чисел на основе линейного когнруэнтного метода!". А оказывается что по настоящему случайную последовательность таким образом создать нельзя. Крах надежд! Депрессия и вот ты уже на пороге первой стадии алкоголизма. Этот груз невозможности реализовать свои желания будет давить на тебя всю оставшуюся жизнь... Что-то я отвлёкся. Продолжим.
Реализация генерации случайных чисел
Теперь, когда мы рассмотрели теоретические вопросы, касающиеся генераторов случайных чисел, давайте посмотрим на реализацию, тем более она довольно проста.
Для последовательности рассмотренной выше, генератор будет выглядеть так:
int rnd() { int m = 10, a = 2, c = 3; static int x = x0; // x объявляется статической переменной x = ((a * x) + c) % m; // формула x0 = x; return x; }
X0 объявляется как глобальная переменная:
int x0 = 6;
Это самый примитивный вариант генератора. В реальности такое чудовище использовать нельзя! Но мы будем это делать!
В данной реализации мы никак не отслеживаем возможность переполнения переменных. Вместо типа int используйте тип _int64 - это целый восьмибайтный тип.
Простые числа
Простые числа - числа которые можно разделить без остатка только на единицу и на само число. Например: 11, 3, 7.
Код Грея
[1] Код Грея представляет собой последовательность чисел, где следующее число отличается от предыдущей одним битом. Т.е. чтобы угадать последовательность, нужно смотреть не на десятичные числа, а на их двоичные эквиваленты.
Вот возрастающая последовательность чисел от 0 до 7 в двоичном коде:
000 0 001 1 010 2 011 3 100 4 101 5 110 6 111 7
Используя код Грея, получим такую последовательность:
000 0 001 1 011 3 010 2 110 6 111 7 101 5 100 4
Простые числа Мерсенна
[3] На данный момент известно 44 числа Мерсенна. Из названия понятно, что данные числа - простые, т.е. делятся только на единицу и на самих себя. Кроме того данные числа должны подчиняться следующей формуле:
Mn = 2n - 1
Где n также простое число. Для первых девяти простых чисел Мерсенна n следующие: {2, 3, 5, 7, 13, 17, 19, 31, 61}.
Поразрядные логические операции
[2] Мы уже рассматривали логические операции && (И) и || (ИЛИ). Существуют также поразрядные логические операции & (поразрядное И) и | (поразрядное ИЛИ). Работате операция следующим образом:
5 && 3; // && всегда возвращает ноль или единицу (здесь 1) 5 & 3; // & возвращает число 101 011 = 001 // в результате единицаТо есть в данной операции сравниваются соответствующие позиции двух чисел и если они равны, то в результируюем числе в данной позиции пишется 1, в противном случае - ноль.
5 | 3; // | возвращает число 101 011 = 111 // в результате 7
Степень числа
Чтобы возвести число в какую-либо степень можно воспользоваться функцией pow(x,y). При этом Вы получите xy. Функция перегруженная, поэтому её можно использовать с разными типами. Для использования функции pow() необходимо включить заголовочный файл math.h.Другие генераторы случайных чисел
Кроме линейного конгруэнтного генератора существует множество других. Например Вихрь Мерсенна, изобретённый двумя японскими учёными (непомню как из зовут) в 1997. У него очень большой (очень большой это мягко сказано) период. Кстати, вихрь Мерсенна использует линейный конгруэнтный генератор для установления начального значения (seed).
Генераторы случайных чисел применяются при шифровании. Но здесь используются специальные генераторы - криптографически защищённые генераторы псевдослучайных чисел. Например блочный шифр (block cipher), потоковый шифр (stream cipher), который был разработан на основе шифра Вернама.
Упражнения:
Реализуйте генераторы случайных чисел на основе линейного когруэнтного метода. В качестве параметров воспользуйтесь следующими:
- m = 232; a = 1664525; c = 1013904223. Данные параметры использовались в примере реализации генератора в одной старой книжке по Фортрану.
- m = 232; a= 214013; c = 2531011; Данные параметры используется в реализации метода в Visual C++. При этом берутся старшие биты 30..16. Берутся именно верхние биты, т.к. в линейном конгруэнтном методе нижние биты гораздо менее случайны. Так как мы пока не умеем брать конкретные биты, можете использовать всё число.
- В качестве m выберите одно из чисел Мерсенна. Начните с малых (23-1,25-1). Попробуйте подставить 231-1 и 261-1.
Данный материал поможет вам понять как работают генераторы случайных чисел. К этому материалу будет продолжение: мы напишем нормальный генератор и рассмотрим различные приёмы работы с генераторами псевдослучайных чисел.