rand ()는 작은 범위에 대해 동일한 숫자를 다시 제공합니다.


9

그리드가 20x20 인 게임을 만들고자 플레이어 (P), 대상 (T) 및 3 명의 적 (X)을 표시합니다. 이것들은 모두를 사용하여 할당 된 X와 Y 좌표를 가지고 있습니다 rand(). 문제는 게임에서 더 많은 포인트 (에너지 리필 등)를 얻으려고하면 범위가 작기 때문에 하나 이상의 다른 포인트와 겹칩니다 (1 ~ 20 포함).

이들은 내 변수이며 값을 할당하는 방법입니다 ( X와 Y COORD가있는 a struct입니다)

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입니다
ratchet freak

3
rand()불쌍한 RNG이며 어쨌든 그러한 작은 범위에서 충돌 을 기대할 필요는 없으며 거의 보장됩니다.
중복 제거기

1
rand()RNG가 거칠다는 것은 사실이지만 단일 플레이어 게임에 적합 할 수 있으며 RNG 품질은 문제가되지 않습니다.
로봇 고 르트

13
품질에 대해 말하는 rand()것은 여기서 관련이없는 것 같습니다. 암호화는 포함되어 있지 않으며 RNG는 이러한 작은 맵에서 충돌을 일으킬 가능성이 큽니다.
Tom Cornebize

2
당신이보고있는 것을 생일 문제라고합니다. 임의의 숫자가 PRNG의 자연 범위보다 작은 범위로 변환되는 경우 같은 숫자의 두 인스턴스를 얻을 확률은 생각보다 훨씬 높습니다. 얼마 전에 나는 여기
ConcernedOfTunbridgeWells

답변:


40

rand()더 나은 RNG 에 대해 불평 하고 추천 하는 사용자 는 난수의 품질에 대해 맞지만 더 큰 그림을 놓치고 있습니다. 난수 스트림의 중복은 피할 수 없으며 인생의 사실입니다. 이것은 생일 문제 의 교훈입니다 .

20 * 20 = 400 가능한 스폰 위치의 그리드에서, 24 개의 엔티티 만 스폰 할 때에도 중복 스폰 지점이 예상됩니다 (50 % 확률). 50 개의 엔티티 (여전히 전체 그리드의 12.5 %에 불과)에서 복제 확률은 95 % 이상입니다. 충돌을 처리해야합니다.

때로는 모든 샘플을 한 번에 그릴 수 있으며 셔플 알고리즘 을 사용하여 n확실한 항목 을 그릴 수 있습니다. 모든 가능성의 목록을 생성하기 만하면됩니다. 전체 가능성 목록이 저장하기에 너무 큰 경우, 지금보다 더 나은 RNG로 한 번에 하나씩 스폰 위치를 생성하고 충돌이 발생할 때 간단히 다시 생성 할 수 있습니다. 가지고 있지만 몇 가지 충돌하는 가능성이 연속으로 많은 충돌이 그리드의 대부분이 채워진 경우에도 기하 급수적으로 가능성이 있습니다.


충돌이 발생할 경우 다시 생성하는 방법에 대해 생각했지만 원하는 항목이 더 있으면 충돌에 대한 조회가 복잡해집니다. 또한 게임에서 포인트가 추가되거나 제거 될 경우 수표를 편집해야합니다. 나는 상당히 경험이 없어서 이에 대한 해결 방법이 있으면 그것을 볼 수 없었습니다.
Rabeez Riaz

7
20x20 연속 (실제) XY 평면과 달리 20x20 바둑판이있는 경우 충돌을 확인하기위한 400 셀 조회 테이블이 있습니다. 이것은 TRIVIAL입니다.
John R. Strohm

@RabeezRiaz 더 큰지도를 사용하면 일부 그리드 기반 데이터 구조 (일부 셀 영역으로 구성된 그리드 및 해당 셀 내부의 모든 항목이 목록에 저장 됨)를 갖게됩니다. 맵이 더 큰 경우에는 rect-tree를 구현합니다.
rwong

2
@RabeezRiaz : 조회가 너무 복잡한 경우 첫 번째 제안을 사용하십시오. 400 개의 가능한 시작 위치를 모두 목록으로 생성하고 무작위 순서로 정렬하고 (알고리즘을 찾음) 필요할 때 처음부터 위치를 사용하십시오. 물건을 생성하기 위해 (이미 얼마나 많이 사용했는지 추적하십시오). 충돌이 없습니다.
RemcoGerlich

2
@RabeezRiaz 적은 수의 임의의 값만 필요한 경우 전체 목록을 섞을 필요가 없으며 필요한 부분을 섞으십시오. 충분한 요소가 있습니다). 실제로 그것은 셔플 링 알고리즘이 어떻게 작동하는지입니다.
Dorus

3

이미 다른 곳에 할당 된 위치에서 새 엔터티를 재생하지 않으려면 프로세스 주위를 약간 변경하면됩니다. 이렇게하면 고유 한 위치가 보장되지만 약간의 오버 헤드가 필요합니다. 단계는 다음과 같습니다.

  1. 지도에서 가능한 모든 위치에 대한 참조 모음을 설정합니다 (20x20지도의 경우 400 개 위치).
  2. 이 400 모음에서 무작위로 위치를 선택하십시오 (rand ()이 잘 작동합니다)
  3. 가능한 위치 콜렉션에서이 가능성을 제거하십시오 (따라서 이제 399 가지 가능성이 있음).
  4. 모든 엔티티가 지정된 위치를 가질 때까지 반복

선택중인 세트에서 위치를 제거하는 한 두 번째 엔티티가 동일한 위치를 수신 할 가능성은 없습니다 (한 번에 둘 이상의 스레드에서 위치를 선택하지 않는 한).

이와 비슷한 현실은 카드 한 벌에서 카드를 뽑는 것입니다. 현재, 당신은 덱을 섞고, 카드를 뽑고, 아래로 표시하고, 뽑은 카드를 다시 덱에 넣고, 다시 섞고 다시 긋고 있습니다. 위의 방법은 카드를 다시 갑판에 넣는 것을 건너 뜁니다.


1

에 관한 rand() % n이상적이지 인

이렇게는 rand() % n비 균일 한 분포를 가지고있다. 값의 수가 20의 배수가 아니기 때문에 특정 값의 불균형 한 수를 얻습니다.

다음 rand()으로 일반적으로 선형 합동 발생기입니다 ( 다른 것들도 많지만 , 이것이 가장 구현 가능성이 높으며 이상적인 매개 변수보다 적습니다 (매개 변수를 선택하는 방법이 많이 있습니다). 이것의 가장 큰 문제는 종종 낮은 비트 ( % 20유형 표현을 가진 비트 )가 무작위가 아니라는 것입니다. 나는 일을 기억 rand()에서 번갈아 가장 낮은 비트 곳 년 전 1으로 0각 호출로는 rand()매우 무작위 아니었다 -.

에서 랜드 (3) 매뉴얼 페이지 :

Linux C 라이브러리의 rand () 및 srand () 버전은 동일합니다.
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이 lrand48 ()의 결과의 하위 비트에 의존하는 경우가 있습니다 (때로는 rn (2)의 경우와 동일). 이로 인해 전체 게임에서 RNG가 제대로 작동하지 않습니다. 이는 특히 사용자 행동이 캐릭터 생성 및 1 차 레벨 생성과 같은 추가 임의성을 도입하기 전에 주목할 만합니다.

위의 2003 년부터는 의도 한 게임을 실행하는 모든 시스템이 좋은 rand () 함수를 가진 최신 Linux 시스템 일 경우가 아니라는 점을 명심해야합니다.

이 작업을 직접 수행하는 경우 코드작성 하고 ent로 출력을 테스트 하여 난수 생성기가 얼마나 좋은지 테스트 할 수 있습니다 .


난수의 속성에

정확히 무작위가 아닌 '무작위'에 대한 다른 해석이 있습니다. 임의의 데이터 스트림에서 동일한 수를 두 번 얻을 수 있습니다. 동전을 뒤집 으면 (무작위), 두 개의 머리를 연속적으로 얻을 수 있습니다. 또는 주사위를 두 번 던지고 같은 숫자를 두 번 연속으로 얻습니다. 또는 룰렛을 돌리고 같은 숫자를 두 번 얻습니다.

숫자의 분포

노래 목록을 재생할 때 '무작위'는 동일한 노래 나 아티스트가 연속해서 두 번 재생되지 않을 것을 의미합니다. 재생 목록이 간주됩니다 두 번 연속 비틀즈 플레이 데 (그것이 비록 '임의하지' 입니다 임의). 네 곡의 재생 목록에 대해 총 여덟 번 재생되었다는 인식 :

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이것은 현재 많은 플랫폼에서 사실이 아닙니다. linux의 rand (3) 매뉴얼 페이지에서 : "Linux C 라이브러리의 rand () 및 srand () 버전은 random (3) 및 srandom (3)과 동일한 난수 생성기를 사용하므로 하위 비트 "고차 비트만큼 임의적이어야합니다." 또한 @delnan이 지적했듯이 PRNG의 품질은 실제 문제가 아닙니다.
찰스 E. 그랜트

4
실제 문제를 해결하지 않기 때문에 이것을 다운 투표하고 있습니다.
user253751

@immibis 그런 다음 다른 대답은 실제 문제를 "해결"하지 않으며 downvoted해야합니다. 질문이 "코드 수정"이 아니라고 생각합니다. "왜 중복 난수가 발생합니까?" 두 번째 질문에는 질문에 대한 답변이 있다고 생각합니다.
Neil

4
RAND_MAX32767 의 가장 작은 값 이더라도 차이는 1639와 다른 숫자를 얻는 1638 가지 방법입니다. OP와 실질적인 차이가 거의 없을 것 같습니다.
Martin Smith

@Neil "Fix my code"는 문제가되지 않습니다.
궤도에서 가벼움 경주

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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.