rand () снова дает те же числа для небольшого диапазона

9

Я пытаюсь сделать что-то вроде игры, в которой у меня есть сетка 20х20, и я показываю игрока (P), цель (T) и трех врагов (X). Все они имеют координаты X и Y, которые назначаются с помощью rand(). Проблема в том, что если я пытаюсь получить больше очков в игре (пополнение для энергии и т. Д.), Они перекрываются с одним или несколькими другими точками, потому что диапазон небольшой (от 1 до 20 включительно).

Это мои переменные и то, как я присваиваю им значения: ( COORDэто structпросто X и Y)

const int gridSize = 20;
COORD player;
COORD target;
COORD enemy1;
COORD enemy2;
COORD enemy3;

//generate player
srand ( time ( NULL ) );
spawn(&player);
//generate target
spawn(&target);
//generate enemies
spawn(&enemy1);
spawn(&enemy2);
spawn(&enemy3);

void spawn(COORD *point)
{
    //allot X and Y coordinate to a point
    point->X = randNum();
    point->Y = randNum();
}

int randNum()
{
    //generate a random number between 1 and gridSize
    return (rand() % gridSize) + 1;
}

Я хочу добавить в игру больше вещей, но вероятность совпадения увеличивается, когда я это делаю. Есть ли способ это исправить?

Рабиз Риаз
источник
8
rand () - плохой RNG
фрик с трещоткой
3
rand()это жалкий ГСЧ, и в любом случае с таким небольшим диапазоном, вам не нужно просто ожидать столкновений, они почти гарантированы.
дедупликатор
1
Хотя это правда, что rand()это паршивая ГСЧ, она, вероятно, подходит для одиночной игры, и качество ГСЧ здесь не проблема.
Gort Робот
13
Говорить о качестве здесь, rand()похоже, неактуально. Криптография не задействована, и любой ГСЧ, скорее всего, вызовет коллизии на такой маленькой карте.
Том Корнебиз
2
То, что вы видите, известно как проблема дня рождения. Если ваши случайные числа преобразуются в диапазон, меньший, чем естественный диапазон PRNG, тогда вероятность получения двух экземпляров одного и того же числа намного выше, чем вы думаете. Некоторое время назад я написал рекламный ролик на эту тему здесь,
ConcernedOfTunbridgeWells

Ответы:

40

Хотя пользователи, которые жалуются rand()и рекомендуют более качественные ГСЧ, правы в отношении качества случайных чисел, им также не хватает более широкой картины. Нельзя избежать дубликатов в потоках случайных чисел, это факт жизни. Это урок проблемы дня рождения .

На сетке из 20 * 20 = 400 возможных позиций появления следует ожидать дублирующую точку появления (вероятность 50%), даже если порождает только 24 объекта. С 50 объектами (все еще только 12,5% всей сетки) вероятность дублирования превышает 95%. Вы должны иметь дело со столкновениями.

Иногда вы можете нарисовать все образцы одновременно, тогда вы можете использовать алгоритм перемешивания, чтобы нарисовать nгарантированно отличные элементы. Вам просто нужно сформировать список всех возможностей. Если полный список возможностей слишком велик для хранения, вы можете генерировать позиции возрождения по одной за раз, как вы это делаете сейчас (только с лучшим ГСЧ), и просто заново генерировать, когда происходит столкновение. Даже если некоторые столкновения вероятны, многие столкновения подряд экспоненциально маловероятны, даже если большая часть сетки заполнена.


источник
Я думал о возрождении в случае столкновения, но если у меня будет больше предметов, как я собираюсь, поиск столкновения будет сложным. Мне также пришлось бы редактировать чеки в случае добавления или удаления точки из игры. Я довольно неопытен, поэтому, если есть обходной путь, я не смог бы его увидеть.
Рабиз Риаз
7
Если у вас есть шахматная доска 20x20, в отличие от непрерывной (реальной) плоскости XY 20x20, то у вас есть таблица поиска на 400 ячеек для проверки на столкновения. Это ТРИВАЛЬНО.
Джон Р. Штром
@RabeezRiaz Если у вас карта большего размера, у вас будет некоторая структура данных на основе сетки (сетка, состоящая из некоторой области ячеек, и каждый элемент внутри этой ячейки сохраняется в списке). Если ваша карта еще больше, вы реализуете прямоугольное дерево.
Rwong
2
@RabeezRiaz: если поиск слишком сложен, воспользуйтесь его первым предложением: создайте список всех 400 возможных начальных местоположений, перетасуйте их так, чтобы они были в случайном порядке (ищите алгоритм), а затем начните использовать местоположения спереди, когда вам нужно для создания материала (следите за тем, сколько вы уже использовали). Нет столкновений.
RemcoGerlich
2
@RabeezRiaz Нет необходимости перетасовывать весь список, если вам нужно только небольшое количество случайных значений, просто перетасуйте нужную часть (например, возьмите случайное значение из списка 1..400, удалите его и повторяйте до у тебя достаточно элементов). На самом деле так работает алгоритм тасования.
Дорус
3

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

  1. Установите коллекцию ссылок на все возможные местоположения на карте (для карты 20x20 это будет 400 местоположений)
  2. Выберите случайное место из этой коллекции из 400 (для этого хорошо подойдет rand ())
  3. Удалить эту возможность из коллекции возможных локаций (теперь у нее есть 399 возможностей)
  4. Повторяйте, пока все объекты не будут указаны

Пока вы удаляете местоположение из набора, из которого вы выбираете, у второго объекта не должно быть шансов получить такое же местоположение (если только вы не выбираете местоположения из более чем одного потока одновременно).

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

Lyise
источник
1

Относительно того, чтобы rand() % nбыть менее чем идеальным

Ведение rand() % nимеет неравномерное распределение. Вы получите непропорциональное количество определенных значений, потому что число значений не кратно 20

Затем, rand()как правило, это линейный конгруэнтный генератор (есть много других , только наиболее вероятно, что он реализован - и с параметрами, меньшими, чем идеальные (есть много способов выбора параметров)). Самая большая проблема в этом состоит в том, что часто младшие биты в нем (те, которые вы получаете с % 20выражением типа) не настолько случайны. Я вспоминаю один rand()из прошлых лет, когда младший бит чередовался с 1на 0каждый вызов rand()- это было не очень случайно.

Из справочной страницы rand (3):

Версии rand () и srand () в библиотеке Linux C используют одинаково
генератор случайных чисел, как random () и srandom (), поэтому нижний порядок
биты должны быть такими же случайными, как биты высшего порядка. Однако на старших
реализации rand () и текущих реализаций на разных
В системах младшего разряда биты гораздо менее случайны, чем старшие
биты заказа. Не используйте эту функцию в приложениях, предназначенных для
портативный, когда нужна хорошая случайность.

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

Нужно просто использовать хорошую библиотеку случайных чисел (которая дает хорошие случайные числа), а затем запрашивать случайные числа в нужном диапазоне.

Пример хорошего кода случайного числа (с 13:00 в связанном видео)

#include <iostream>
#include <random>
int main() {
    std::mt19937 mt(1729); // yes, this is a fixed seed
    std::uniform_int_distribution<int> dist(0, 99);
    for (int i = 0; i < 10000; i++) {
        std::cout << dist(mt) << " ";
    }
    std::cout << std::endl;
}

Сравните это с:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
    srand(time(NULL));
    for (int i = 0; i < 10000; i++) {
        printf("%d ", rand() % 100);
    }
    printf("\n");
}

Запустите обе эти программы и сравните, как часто определенные числа появляются (или не появляются) в этом выводе.

Видео по теме: rand () считается вредным

Некоторые исторические аспекты rand (), вызывающие ошибки в Nethack, которые следует наблюдать и учитывать в собственных реализациях:

  • Nethack RNG Проблема

    Rand () - очень фундаментальная функция для генерации случайных чисел в Nethack. То, как Nethack использует его, ошибочно, или можно утверждать, что lrand48 () генерирует дрянные псевдослучайные числа. (Однако lrand48 () - это библиотечная функция, использующая определенный метод PRNG, и любая программа, которая ее использует, должна учитывать недостатки этого метода.)

    Ошибка в том, что Nethack полагается (иногда исключительно, как в случае с rn (2)) на младшие биты результатов lrand48 (). Из-за этого ГСЧ во всей игре работает плохо. Это особенно заметно прежде, чем действия пользователя привнесут дополнительную случайность, то есть в генерацию персонажа и создание первого уровня.

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

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


О свойствах случайных чисел

Существуют и другие интерпретации «случайного», которые не совсем случайны. В случайном потоке данных вполне возможно получить одно и то же число дважды. Если вы подбросите монету (случайно), вполне возможно получить две головы подряд. Или бросьте кубик дважды и получите одно и то же число дважды подряд. Или вращая колесо рулетки и получая одно и то же число дважды там.

Распределение чисел

При воспроизведении списка песен люди ожидают, что «случайный» означает, что одна и та же песня или исполнитель не будут воспроизводиться второй раз подряд. Имея список воспроизведения играть The Beatles дважды подряд считается «не случайным» (хотя это является случайным). Восприятие, что для списка воспроизведения из четырех песен играли в общей сложности восемь раз:

1 3 2 4 1 2 4 3

является более «случайным», чем:

1 3 3 2 1 4 4 2

Подробнее об этом для «перемешивания» песен: как перемешать песни?

На повторных значениях

Если вы не хотите повторять значения, следует рассмотреть другой подход. Сгенерируйте все возможные значения и перемешайте их.

Если вы звоните rand()(или любой другой генератор случайных чисел), вы звоните с заменой. Вы всегда можете получить один и тот же номер дважды. Один из вариантов - отбрасывать значения снова и снова, пока вы не выберете то, что соответствует вашим требованиям. Я укажу, что это имеет недетерминированное время выполнения, и возможно, что вы окажетесь в ситуации, когда есть бесконечный цикл, если вы не начнете выполнять более сложную обратную трассировку.

Список и выбор

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

шарканье

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

Сообщество
источник
3
Next, rand() is typically a linear congruential generatorЭто не так на многих платформах сейчас. Из справочной страницы raux (3) в linux: «Версии rand () и srand () в библиотеке Linux C используют тот же генератор случайных чисел, что и random (3) и srandom (3), поэтому биты младшего разряда должен быть случайным, как биты старшего разряда. " Кроме того, как отмечает @delnan, качество PRNG здесь не является реальной проблемой.
Чарльз Грант
4
Я не одобряю это, потому что это не решает актуальную проблему.
user253751 20.07.15
@immibis Тогда другой ответ не «решает» актуальную проблему и должен быть опущен. Я думаю, что вопрос не «исправить мой код», а «почему я получаю дубликаты случайных чисел?» На второй вопрос, я полагаю, ответ на этот вопрос.
Нил
4
Даже при наименьшем значении RAND_MAX32767 разница составляет 1638 возможных способов получения одних чисел против 1639 для других. Кажется, вряд ли будет иметь большое практическое значение для ОП.
Мартин Смит
@Neil "Исправить мой код" не вопрос.
Гонки легкости на орбите
0

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

Этот метод имеет то преимущество, что полностью избегает перекрытия случайно выбранных ячеек.

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

Вот пример того, как вы можете это сделать:

#include <algorithm>
#include <iostream>
#include <vector>

#define NUMBER_OF_SPAWNS 20
#define WIDTH 20
#define HEIGHT 20

typedef struct _COORD
{
  int x;
  int y;
  _COORD() : x(0), y(0) {}
  _COORD(int xp, int yp) : x(xp), y(yp) {}
} COORD;

typedef struct _spawnCOORD
{
  float rndValue;
  COORD*coord;
  _spawnCOORD() : rndValue(0.) {}
} spawnCOORD;

struct byRndValue {
  bool operator()(spawnCOORD const &a, spawnCOORD const &b) {
    return a.rndValue < b.rndValue;
  }
};

int main(int argc, char** argv)
{
  COORD map[WIDTH][HEIGHT];
  std::vector<spawnCOORD>       rndSpawns(WIDTH * HEIGHT);

  for (int x = 0; x < WIDTH; ++x)
    for (int y = 0; y < HEIGHT; ++y)
      {
        map[x][y].x = x;
        map[x][y].y = y;
        rndSpawns[x + y * WIDTH].coord = &(map[x][y]);
        rndSpawns[x + y * WIDTH].rndValue = rand();
      }

  std::sort(rndSpawns.begin(), rndSpawns.end(), byRndValue());

  for (int i = 0; i < NUMBER_OF_SPAWNS; ++i)
    std::cout << "Case selected for spawn : " << rndSpawns[i].coord->x << "x"
              << rndSpawns[i].coord->y << " (rnd=" << rndSpawns[i].rndValue << ")\n";
  return 0;
}

Результат:

root@debian6:/home/eh/testa# ./exe 
Case selected for spawn : 11x15 (rnd=6.93951e+06)
Case selected for spawn : 14x1 (rnd=7.68493e+06)
Case selected for spawn : 8x12 (rnd=8.93699e+06)
Case selected for spawn : 18x13 (rnd=1.16148e+07)
Case selected for spawn : 1x0 (rnd=3.50052e+07)
Case selected for spawn : 2x17 (rnd=4.29992e+07)
Case selected for spawn : 9x14 (rnd=7.60658e+07)
Case selected for spawn : 3x11 (rnd=8.43539e+07)
Case selected for spawn : 12x7 (rnd=8.77554e+07)
Case selected for spawn : 19x0 (rnd=1.05576e+08)
Case selected for spawn : 19x14 (rnd=1.10613e+08)
Case selected for spawn : 8x2 (rnd=1.11538e+08)
Case selected for spawn : 7x2 (rnd=1.12806e+08)
Case selected for spawn : 19x15 (rnd=1.14724e+08)
Case selected for spawn : 8x9 (rnd=1.16088e+08)
Case selected for spawn : 2x19 (rnd=1.35497e+08)
Case selected for spawn : 2x16 (rnd=1.37807e+08)
Case selected for spawn : 2x8 (rnd=1.49798e+08)
Case selected for spawn : 7x16 (rnd=1.50123e+08)
Case selected for spawn : 8x11 (rnd=1.55325e+08)

Просто измените NUMBER_OF_SPAWNS, чтобы получить больше или меньше случайных ячеек, это не изменит время вычисления, необходимое для задачи.

KwentRell
источник
«а затем, чтобы отсортировать их все» - я полагаю, вы имеете в виду «shuffle»
Я немного закончил объяснение. Теперь должно быть понятнее.
КвентРелл