루프없이 많은 주사위 롤을 정확하게 시뮬레이션합니까?


14

게임에서 주사위를 많이 굴리면 임의의 숫자 생성기를 루프로 호출 할 수 있습니다. 그러나 주사위를 자주 굴리면 분포 곡선 / 히스토그램을 얻을 수 있습니다. 그래서 내 질문은 그 분포에 맞는 숫자를 줄 수있는 멋진 간단한 계산이 있습니까?

예 : 2D6-점수-확률 %

2-2.77 %

3-5.55 %

4-8.33 %

5-11.11 %

6-13.88 %

7-16.66 %

8-13.88 %

9-11.11 %

10-8.33 %

11-5.55 %

12-2.77 %

위의 내용을 알면 단일 d100을 굴려서 정확한 2D6 값을 계산할 수 있습니다. 그러나 일단 10D6, 50D6, 100D6, 1000D6으로 시작하면 많은 처리 시간을 절약 할 수 있습니다. 그래서 이것을 빨리 할 수있는 튜토리얼 / 방법 / 알고리즘이 있어야합니까? 주식 시장, 카지노, 전략 게임, 난쟁이 요새 등에 유용 할 것입니다.이 함수를 몇 번만 호출하고 몇 가지 기본 수학을 수행하는 데 몇 시간이 걸리는 완전한 전략적 전투 결과를 시뮬레이션 할 수 있다면 어떨까요?


5
1000d6에서도 루프가 눈에 띄지 않는 최신 PC에서는 충분히 빠르므로 조기에 최적화 될 수 있습니다. 클리어 루프를 불투명 한 공식으로 교체하기 전에 항상 프로파일 링을 시도하십시오. 즉, 알고리즘 옵션이 있습니다. 예에서 주사위와 같은 불연속 확률에 관심이 있습니까, 아니면 연속 확률 분포로 모형화하는 것이 허용됩니까 (2.5와 같은 소수의 결과가 가능할 수 있습니까)?
DMGregory

DMGregory가 정확하고 1000d6을 계산한다고해서 많은 프로세서 호그가되지는 않습니다. 그러나 (일부 영리한 작업으로) 이항 분포 ( Binomial Distribution) 라는 것이 있는데, 원하는 결과를 얻을 수 있습니다. 또한 임의의 롤 규칙 세트에 대한 확률을 찾으려면 적당한 언어를 가진 TRoll 을 시도 하십시오. 주사위 세트를 굴리는 방법을 지정하기 위해 설정되며 가능한 모든 결과에 대한 모든 확률을 계산합니다.
Draco18s는 더 이상 SE

푸 아송 분포를 사용하십시오.
Luis Masuelli

1
주사위의 설정이 자주 출시되는 위해 당신은 것입니다 아마 분포 곡선 / 히스토그램을 얻는다. 그것은 중요한 차이점입니다. 주사위는 연속으로 백만 6 초를 굴릴 수 있지만, 가능하지는 않습니다.
Richard Tingle

@RichardTingle 정교하게 할 수 있습니까? 분포 곡선 / 히스토그램에는 "백만 개의 6s 행"경우도 포함됩니다.
amitp

답변:


16

위의 의견에서 언급했듯이 코드를 복잡하게 만들기 전에 이것을 프로파일 링하는 것이 좋습니다. 빠른 for루프 합산 주사위는 복잡한 수학 공식 및 테이블 작성 / 검색보다 이해하고 수정하기가 훨씬 쉽습니다. 중요한 문제를 해결하려면 항상 먼저 프로파일 링하십시오. ;)

즉, 한 번에 복잡한 확률 분포를 샘플링하는 두 가지 주요 방법이 있습니다.


1. 누적 확률 분포

하나의 균일 한 무작위 입력 만 사용하여 연속 확률 분포에서 샘플링하는 깔끔한 트릭 이 있습니다 . 그것은 함께 할 수있다 누적 분포 함수를 그 "값을 얻기없는 확률 무엇 답변 더 큰 X보다가?"

이 함수는 감소하지 않고 0에서 시작하여 도메인에서 1로 증가합니다. 6 면체 주사위 2 개를 합한 예는 다음과 같습니다.

2d6에 대한 확률, 누적 분포 및 역의 그래프

누적 분포 함수에 계산하기 편리한 역수가있는 경우 (또는 베 지어 곡선과 같은 부분 함수로 근사 할 수있는 경우)이를 사용하여 원래 확률 함수에서 샘플링 할 수 있습니다.

역함수는 0에서 1 사이의 도메인을 원래 랜덤 프로세스의 각 출력에 매핑 된 간격으로 파 셀링하고 각 영역의 원래의 확률과 일치하는 포획 영역을 처리합니다. (이것은 연속 분포의 경우 무한히 사실입니다. 주사위 롤과 같은 이산 분포의 경우 신중한 반올림을 적용해야합니다)

이것을 사용하여 2d6을 에뮬레이트하는 예는 다음과 같습니다.

int SimRoll2d6()
{
    // Get a random input in the half-open interval [0, 1).
    float t = Random.Range(0f, 1f);
    float v;

    // Piecewise inverse calculated by hand. ;)
    if(t <= 0.5f)
    {
         v = (1f + sqrt(1f + 288f * t)) * 0.5f;
    }
    else
    {
         v = (25f - sqrt(289f - 288f * t)) * 0.5f;
    }

    return floor(v + 1);
}

이것을 다음과 비교하십시오.

int NaiveRollNd6(int n)
{
    int sum = 0;
    for(int i = 0; i < n; i++)
       sum += Random.Range(1, 7); // I'm used to Range never returning its max
    return sum;
}

코드 선명도와 유연성의 차이에 대해 무엇을 의미합니까? 순진한 방법은 루프로 순진하지만 짧고 간단하며 작동 방식을 즉시 알 수 있으며 다양한 다이 크기와 수로 쉽게 확장 할 수 있습니다. 누적 배포 코드를 변경하려면 사소한 수학이 필요하며 명확한 실수없이 쉽게 깨져서 예기치 않은 결과가 발생할 수 있습니다. (내가 위에서 만들지 않았 으면 좋겠다)

따라서 명확한 루프를 없애기 전에 실제로 이런 종류의 희생에 가치가있는 성능 문제인지 반드시 확인하십시오.


2. 별칭 방법

누적 분포 함수의 역수를 간단한 수학 식으로 표현할 수있을 때 누적 분포 방법이 효과적이지만 항상 쉬운 것은 아닙니다. 불연속 분포의 신뢰할 수있는 대안 은 Alias ​​Method 입니다.

이를 통해 두 개의 독립적으로 균일하게 분포 된 임의의 입력을 사용하여 임의의 이산 확률 분포에서 샘플링 할 수 있습니다.

왼쪽 아래의 것과 같은 분포를 취하고 ( 상대 가중치에 관심이있는 Alias ​​Method의 경우 면적 / 무게가 1로 합산되지 않을까 걱정하지 마십시오 ) 다음과 같은 표로 변환하여 작동합니다 오른쪽 위치 :

  • 각 결과에 대해 하나의 열이 있습니다.
  • 각 열은 최대 두 부분으로 나뉘며 각 부분은 원래 결과 중 하나와 연결됩니다.
  • 각 결과의 상대적 면적 / 무게가 유지됩니다.

분포를 조회 테이블로 변환하는 별칭 방법의 예

( 샘플링 방법에 대한이 훌륭한 기사의 이미지를 기반으로 한 다이어그램 )

코드에서 우리는 각 열에서 대체 결과를 선택할 확률을 나타내는 두 개의 테이블 (또는 두 개의 속성을 가진 객체 테이블)과 대체 결과의 동일성 (또는 "별칭")으로이를 나타냅니다. 그런 다음 분포에서 표본 추출 할 수 있습니다.

int SampleFromTables(float[] probabiltyTable, int[] aliasTable)
{
    int column = Random.Range(0, probabilityTable.Length);
    float p = Random.Range(0f, 1f);
    if(p < probabilityTable[column])
    {
        return column;
    }
    else
    {
        return aliasTable[column];
    }
}

여기에는 약간의 설정이 포함됩니다.

  1. 가능한 모든 결과의 상대 확률을 계산하십시오 (따라서 1000d6을 굴릴 경우 1000에서 6000까지 모든 합계를 얻는 방법의 수를 계산해야합니다)

  2. 각 결과에 대한 항목으로 테이블 쌍을 작성하십시오. 전체 방법은이 답변의 범위를 벗어나므로 Alias ​​Method 알고리즘에 대한이 설명을 참조하는 것이 좋습니다 .

  3. 이 분포에서 새로운 랜덤 다이 롤이 필요할 때마다 해당 테이블을 저장하고 다시 참조하십시오.

이것은 시공간 상충 관계 입니다. 사전 계산 단계는 다소 철저하며, 결과 수에 비례하여 메모리를 따로 보관해야합니다 (1000d6의 경우에도 한 자리 킬로바이트를 사용하므로 수면을 잃을 것은 없습니다). 분포가 아무리 복잡하더라도 상수 시간입니다.


나는 그 방법들 중 하나 또는 다른 방법이 다소 유용 할 수 있기를 바랍니다 (또는 순진한 방법의 단순성이 반복하는 데 가치가 있다고 확신합니다);)


1
멋진 답변입니다. 나는 순진한 접근법을 좋아합니다. 오류의 여지가 적고 이해하기 쉽습니다.
bummzack

참고 로이 질문은 임의의 reddit 질문에서 복사하여 붙여 넣습니다.
Vaillancourt

불완전 성을 위해 이것이 @AlexandreVaillancourt가 말하는 레딧 스레드 라고 생각 합니다 . 거기에 대한 대답은 주로 루핑 버전을 유지하거나 (시간 비용이 합리적 일 수 있다는 증거가 있음) 정규 / 가우시안 분포를 사용하여 많은 수의 주사위를 근사화하는 것입니다.
DMGregory

앨리어스 방법의 경우 +1, 그에 대해 아는 사람은 거의없는 것 같습니다. 실제로 이러한 유형의 확률 선택 상황에 대한 이상적인 솔루션이며 가우시안 솔루션에 대해서는 +1입니다. 아마도 "더 나은" 성능과 공간 절약에만 관심이 있다면 대답하십시오.
whn

0

안타깝게도이 방법으로 성능이 향상되지는 않습니다.

난수가 어떻게 생성되는지에 대한 오해가있을 수 있다고 생각합니다. 아래의 예제를 보자 [Java] :

Random r = new Random();
int n = 20;
int min = 1; //arbitrary
int max = 6; //arbitrary
for(int i = 0; i < n; i++){
    int randomNumber = (r.nextInt(max - min + 1) + min)); //silly maths
    System.out.println("Here's a random number: " + randomNumber);
}

이 코드는 1에서 6 사이의 임의의 숫자를 인쇄하여 20 회 반복됩니다 (포함). 이 코드의 성능에 대해 이야기 할 때 Random 객체 (만들었을 때 컴퓨터의 내부 시계를 기준으로 의사 난수 정수 배열을 생성하는 데 소요됨)를 생성하는 데 약간의 시간이 소요됩니다. 각 nextInt () 호출에서 조회합니다. 각 '롤'은 일정한 시간 작업이므로 시간이 매우 저렴합니다. 또한 min에서 max까지의 범위는 중요하지 않습니다 (즉, 컴퓨터가 d6100을 굴리는 것만 큼 d6을 굴리는 것이 쉽습니다). 시간 복잡성 측면에서 말하면, 솔루션의 성능은 단순히 O (n)입니다. 여기서 n은 주사위 수입니다.

또는 단일 d100 (또는 해당 문제의 경우 d10000) 롤을 사용하여 원하는 개수의 d6 롤을 추정 할 수 있습니다. 이 방법을 사용하여 롤링하기 전에 먼저 s [얼굴 수] * n [다이스 수] 백분율을 계산해야합니다 (기술적으로는 s * n-n + 1 %이므로 대략적으로 나눌 수 있어야합니다). 2d6 롤 시뮬레이션 예제에서는 11 %와 6이 고유 한 것으로 계산되었습니다). 롤링 후 이진 검색을 사용하여 롤이 어느 범위에 속하는지 알아낼 수 있습니다. 시간 복잡성 측면에서이 솔루션은 O (s * n) 솔루션으로 평가됩니다. 여기서 s는 변의 수이고 n은 주사위의 수입니다. 보다시피, 이것은 이전 단락에서 제안 된 O (n) 솔루션보다 느립니다.

거기에서 외삽하면 1000d20 롤을 시뮬레이션하기 위해이 두 프로그램을 모두 만들었다 고 가정 해보십시오. 첫 번째는 단순히 1,000 번 굴립니다. 두 번째 프로그램은 다른 작업을 수행하기 전에 먼저 19,001 % (잠재적 범위 1,000 ~ 20,000)를 결정해야합니다. 따라서 메모리 조회가 부동 소수점 연산보다 몇 배나 비싼 이상한 시스템을 사용하지 않는 한 각 롤마다 nextInt () 호출을 사용하는 것이 좋습니다.


2
위의 분석은 정확하지 않습니다. Alias ​​Method 에 따라 확률 및 앨리어스 테이블을 생성하기 위해 약간의 시간을 미리 설정하면 일정한 시간 (2 개의 난수 및 테이블 조회)으로 임의의 이산 확률 분포에서 샘플링 할 수 있습니다. 따라서 5 개의 주사위 롤이나 500 개의 주사위 롤을 시뮬레이션하려면 일단 테이블이 준비되면 같은 양의 작업이 필요합니다. 이것은 모든 샘플에 대해 많은 수의 주사위를 반복하는 것보다 점진적으로 빠르지 만 반드시 문제에 대한 더 나은 해결책은 아닙니다. ;)
DMGregory

0

주사위 조합을 저장하고 싶다면 좋은 해결책은 해결책이 있다는 것입니다. 나쁜 점은 우리 컴퓨터가 이런 종류의 문제와 관련하여 어떻게 든 제한되어 있다는 것입니다.

좋은 뉴스:

이 문제에 대한 결정적인 접근 방식이 있습니다.

1 / 주사위 그룹의 모든 조합을 계산

2 / 각 조합의 확률 결정

3 /이 목록에서 오지를 던지는 대신 결과를 검색합니다.

나쁜 소식 :

반복과의 조합 수는 다음 공식으로 주어집니다

Γ케이=(+케이1케이)=(+케이1)!케이! (1)!

( 프랑스 위키 백과에서 ) :

반복과의 결합

즉, 예를 들어 150 개의 오지에서는 698'526'906 조합이 있습니다. 확률을 32 비트 부동 소수점으로 저장한다고 가정하면 2,6GB의 메모리가 필요하며 인덱스에 대한 메모리 요구 사항을 추가해야합니다.

계산 용어로, 조합 수는 컨볼 루션 (convolution)에 의해 계산 될 수 있는데, 이는 편리하지만 메모리 제약을 해결하지 못한다.

결론적으로, 많은 수의 오지에 대해서는 각 조합과 관련된 확률을 미리 계산하는 대신 오지를 던져 결과를 관찰하는 것이 좋습니다.

편집하다

그러나 오지의 합계에만 관심이 있으므로 훨씬 적은 자원으로 확률을 저장할 수 있습니다.

컨벌루션을 사용하여 각 주사위 합에 대한 정확한 확률을 계산할 수 있습니다.

에프나는(미디엄)=에프1()에프나는1(미디엄)

그런 다음 1/6의 각 결과에서 1/6부터 시작하여 주사위 수에 대한 모든 올바른 확률을 구성 할 수 있습니다.

다음은 일러스트레이션 용으로 작성한 대략적인 Java 코드입니다 (실제로 최적화되지는 않음).

public class DiceProba {

private float[][] probas;
private int currentCalc;

public int getCurrentCalc() {
    return currentCalc;
}

public float[][] getProbas() {
    return probas;
}

public void calcProb(int faces, int diceNr) {

    if (diceNr < 0) {
        currentCalc = 0;
        return;
    }

    // Initialize
    float baseProba = 1.0f / ((float) faces);
    probas = new float[diceNr][];
    probas[0] = new float[faces + 1];
    probas[0][0] = 0.0f;
    for (int i = 1; i <= faces; ++i)
        probas[0][i] = baseProba;

    for (int i = 1; i < diceNr; ++i) {

        int maxValue = (i + 1) * faces + 1;
        probas[i] = new float[maxValue];

        for (int j = 0; j < maxValue; ++j) {

            probas[i][j] = 0;
            for (int k = 0; k <= j; ++k) {
                probas[i][j] += probability(faces, k, 0) * probability(faces, j - k, i - 1);
            }

        }

    }

    currentCalc = diceNr;

}

private float probability(int faces, int number, int diceNr) {

    if (number < 0 || number > ((diceNr + 1) * faces))
        return 0.0f;

    return probas[diceNr][number];

}

}

원하는 매개 변수로 calcProb ()를 호출 한 다음 결과에 대한 proba 테이블에 액세스하십시오 (첫 번째 색인 : 1 주사위의 경우 0, 2 개의 주사위의 경우 1).

랩톱에서 1'000D6으로 확인했는데 1에서 1,000 개의 오지에서 가능한 모든 오지의 합계를 계산하는 데 10 초가 걸렸습니다.

사전 계산 및 효율적인 스토리지를 통해 많은 오지에 대한 빠른 답변을 얻을 수 있습니다.

도움이 되길 바랍니다.


3
OP는 주사위 합의 값만 찾고 있기 때문에이 조합 연산은 적용되지 않으며 확률 테이블 항목의 수는 주사위의 크기와 주사위 수에 따라 선형 적으로 증가합니다.
DMGregory

당신이 맞아요! 내 답변을 편집했습니다. 우리는 항상 영리하다;)
elenfoiro78

분할 및 정복 접근 방식을 사용하면 효율성을 약간 높일 수 있다고 생각합니다. 10d6에 대한 테이블을 자체와 연결하여 20d6에 대한 확률 테이블을 계산할 수 있습니다. 10d6 우리는 5d6 테이블을 그 자체와 관련 시켜서 찾을 수 있습니다. 5d6 우리는 2d6 & 3d6 테이블을 좁혀서 찾을 수 있습니다. 이 방법으로 절반으로 진행하면 1-20에서 대부분의 테이블 크기 생성을 건너 뛰고 흥미로운 테이블 크기에 집중할 수 있습니다.
DMGregory

1
그리고 대칭을 사용하십시오!
elenfoiro78
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.