Укрощение rand() и random()
Автор: © Ken O. Burtch
|
Внизу Научного Центра Онтарио в Торонто (Канада) расположено большое круговое устройство, сделанное из тонких прутов стали. Любознательные наблюдатели могут взять биллиардные шары, находяшиеся там для этой цели, и пустить их свободно по устройству. Шары со свистом летят по направляющим, с лязгом рикошетят от штырей, захватываются центробежной силой и поднимаются к потолку. В некоторых местах шары выбирают одну направляющую или другую совершенно случайно. Как получается, что эта несиловая конструкция, созданная по жесткому образцу, выдает непредсказуемые результаты? Написание программ, использующих случайные числа, требует понимания методов оценки ошибки, теории вероятностей, статистики и других специализированных математических дисциплин. Похоже на какую-то занудную чушь? Случайные числа нужны для придания вашим программам эффекта неожиданности без вовлечения core dump. Это настоящий полигон для захватывающих дух экспериментов.
Случайно, но не по-настоящему случайно
К сожалению, реальные случайные числа могут быть неожиданно необъективны. Как говорится в старом высказывании "реальность - это особый случай". Вместо этого компьютеры полагаются на математику для генерации равномерно распределенных чисел (т.е. случайных, но не слишком). Они "псевдо-случайные", сгенерированы математическими функциями, которые создают, по-видимому, не повторяющуюся последовательность В конце концов, числа в последовательности будут встречаться одинаково часто, без какого-либо преимущества одного над другим. Стандартная в Linux С-библиотека (stdlib.h) содержит две встроенные функции для случайных чисел. Первая, rand(), возвращает случайную последовательность между 0 и RAND_MAX. Если мы наберем printf( " rand() is %d\n", rand() );
rand() вернет подобные значения rand() is 1750354891 Каждое обращение вернет новое, случайно выбранное положительное целое число (типа integer). Другая стандартная библиотечная функция random() вернет положительное длинное целое число (типа long integer). В Linux и целочисленные, и длинные целые числа имеют одинаковый размер. random() обладает и некоторыми другими свойствами, которые обсудим далее. Существуют также и другие, уже устаревшие, функции для получения случайных чисел * drand()/erand() возвращает случайное двойное
между 0..1. Они обеспечивают обратную совместимость с другими клонами UNIX. rand() и random(), конечно, совершенно бесполезны сами по себе и редко вызываются непосредственно. Не так часто мы ищем числа между 0 и действительно большим числом: числа должны принадлежать, в соответствии с поставленной проблемой, заданному диапазону. Чтобы приручить rand(), его значения должны быть промасштабированы в приемлемом диапазоне, например между 0 и заданным максимумом. Неплохо работает модульный оператор (%): когда число поделено, остаток между 0 и 1 меньше оригинала. Добавление 1 к модулю дает искомый диапазон. int rnd( int max ) {
Здесь одна линейная функция вернет числа между 1 и заданым максимумом. rnd(10) вернет числа между 1 и 10, rnd(50) вернет числа между 1 и 50. Реальные события могут моделироваться выбором соответствующих значений для получения определенных результатов. Бросок монеты - это rnd(2)==1 для решки и rnd(2)==2 для орла Бросок игрального кубика - rnd(6)+rnd(6) В различных Linux руководствах по rand() рекомендуется брать "верхние биты" (т.е. использовать деление вместо модуля), потому что это имеет тенденцию быть более случайным. Однако функция rnd(), упомянутая выше, генерирует числа со степенью случайности, приемлемой для большинства приложений. Следующая тестовая программа генерирует 100 чисел между 1 и 10, считая как часто каждое число появляется в последовательности. Если бы числа были совершенно равноправны, каждое из них появилось бы по 10 раз. int
graph[11];
for (i=1; i<=10; i++) Запустив эту подпрограмму, мы получим следующее: for rnd(), graph[1..10] is 7 12 9 8 14 9 16 5 11 9 Функция rand() в Linux прилагает серьезные усилия, чтобы сгенерировать высококачественную случайную последовательность, и поэтому использует существенное количество процессорного времени. Если необходимо быстро сгенерировать случайную последовательность среднего качества, используйте что-нибудь подобное: unsigned int seed = 0; int fast_rnd( int max ) {
seed
= seed * multiplier + offset; Эта функция жертвует точностью ради скорости: она производит случайную последовательность не так математически верно, как rnd(), но использует только несколько небольших вычислений. В идеале, смещение и множитель должны быть такими простыми числами, чтобы минимальное количество чисел имело преимущество над остальными. Замена rnd на fast_rnd() в тестовых функциях все еще дает разумное приближение к rand() for fast_rnd(), graph[1..10] is 11 4 4 1 8 8 5 7 6 5
Контроль последовательности
seed = room_number;
Seed для rand() в Linux устанавливается посредством srand(). Например, srand( 4 ); установит rand() seed в 4. Существуют два способа управлять последовательностью в другой функции Linux - random(). Первый - srandom(), подобен srand(), устанавливает seed для random(). Второй способ актуален, если вы нуждаетесь в большей точности: Linux обеспечивает две функции, чтобы управлять быстродействием и точностью random(). С помощью initstate() вы можете задать random() и начальное число, и буфер для хранения промежуточных результатов. Буфер может быть размером 8, 32, 64, 128 или 256. Больший буфер даст более случайные числа, но потребует больше времени для расчетов. char
state[256]; /* 256 байт буфер */ initstate( seed, state, 256 ); получаем using a 256 byte state, we get 510644794
Вы можете переключать
состояние random() посредством setstate() совместно srandom() для установки
seed в определенное значение. oldstate = setstate( newstate ); Если вы не изменяете seed при запуске программы, ваши случайные числа будут всегда те же самые. Для создания изменяющейся случайной последовательности seed должно быть установлено в некоторое значение, взятое вне программы. Хороший прием - использование значения, возвращаемого функцией time(), использующей time.h. srand( time( NULL ) ); Так как время постоянно меняется, это даст вашей программе новую последовательность случайных чисел при каждом новом запуске.
Перетасовывание списков
Так каков же лучший метод перетасовки списка? Разрезание печатных листов? Сомневаюсь. Обмен случайных элементов несколько тысяч раз?. Эффективно, но медленно и не гарантирует, что все элементы будут иметь шанс на перемещение. Лучше будет взять каждый элемент списка и поменять его местами с каким-нибудь другим. Например, предположим, что мы имеем список из 52 игральных карт с номерами от 0 до 51. Для перетасовки карт сделаем следующее: int
deck[ 52 ]; for ( i=0; i<52; i++ )
В результате получим расклад до и после: Было 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48 49 50 51
Различные типы случайностей
Небольшие неожиданные события подобно этому описываются колоколообразной кривой Гаусса (называемой нормальным распределением в статистике). Создание случайных чисел, удовлетворяющих таким сложным условиям, может показаться неординарной задачей, но в действительности это не так. Наша функция rnd() уже производит неплохие равномерно распределенные "неожиданные" события, нам не нужны талмуды по статистике с формулами для генерации нормально распределенных случайных чисел. Все что нужно, так это несколько раз вызвать rnd() и взять среднее значение, моделируя нормальное распределение. int normal_rnd( int max ) { } Используя normal_rnd() в тестовой функции, мы получаем значения, которые распределены в средней точке между 1 и max: for normal_rnd(), graph[1..10] is 0 0 4 26 37 23 10 0 0 0 Нормальные случайные числа могут быть использованы, чтобы сделать игру более правдоподобной, делая поведение врагов более устойчивым. Для чисел, искаженных к концу диапазона, мы можем создать low_rnd() с приоритетом около 1. int low_rnd( int max ) {
candidate = rnd( max );
В каждой рекурсии low_rnd() делит диапазон пополам. Вычитая наименьшее случайное число от наибольшего в диапазоне, мы можем записать соответствующие значения high_rnd(), где приоритет отдается числам, расположенным около max: int high_rnd( int max ) {
Искажения легко обнаруживаются с помощью тестовой программы: для low_rnd(), graph[1..10] is 36 15 11 8 9 3 4 3 3 8
Случайные условные операторы
int odds( int percent ) {
Эта функция возвращает значение "Истина" в течение указанного процент от заданного времени, тем самым облегчая объединение с условными операторами типа if.
if ( odds( 50 ) ) Стандартые в С-библиотеке функции rand() и random() обеспечивают программную генерация равномерно распределенных случайных чисел. Последовательность и точность могут управляться другими библиотечными функциями и распределение чисел может быть изменено простыми функциями. Случайные числа могут добавить непредсказуемость программе и, конечно, это отправная точка для игрового азарта в компьютерных играх.
|
Copyright © 2001, Ken O. Burtch. Copying license http://www.linuxgazette.com/copying.html Published in Issue 63 of Linux Gazette, Mid-February (EXTRA) 2001 |
Вернуться на главную страницу |