ループなしで多くのサイコロを正確にシミュレートしますか?


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%

したがって、上記を知っていれば、1つのd100をロールアウトして、正確な2D6値を算出できます。ただし、10D6、50D6、100D6、1000D6で開始すると、処理時間を大幅に節約できます。だから、これを高速に行うことができるチュートリアル/メソッド/アルゴリズムが必要ですか?株式市場、カジノ、戦略ゲーム、ドワーフ要塞などに便利でしょう。この関数といくつかの基本的な数学を数回呼び出すだけで何時間もかかる完全な戦略バトルの結果をシミュレートできたらどうでしょうか。


5
1000 d6であっても、ループは最新のPCでは十分に高速であるため、気付かない可能性が高いため、これは時期尚早な最適化の可能性があります。明確なループを不透明な式に置き換える前に、常にプロファイリングを試してください。ただし、アルゴリズムのオプションがあります。あなたの例のサイコロのような離散確率に興味がありますか、または連続確率分布としてそれらをモデル化することは受け入れられますか(2.5のような分数の結果が可能かもしれません)?
DMGregory

DMGregoryが正しい、1000d6を計算することは、プロセッサを大量に消費することにはなりません。ただし、(巧妙な作業で)興味のある結果を得る二項分布と呼ばれるものがあります。また、任意のロールルールセットの確率を見つけたい場合は、控えめな言語のTRollを試してください。サイコロのセットを振る方法を指定するために設定され、すべての可能な結果の確率をすべて計算します。
Draco18sはSEを信頼しなくなり

ポアソン分布を使用します:p。
ルイスマスエリ

1
サイコロのセットが頻繁に転がされると、おそらく分布曲線/ヒストグラムが得られます。それは重要な違いです。サイコロが100万の6を連続して転がす可能性は低いですが、可能です
リチャードティングル

@RichardTingle詳しく説明してもらえますか?分布曲線/ヒストグラムには、「6万の連続」のケースも含まれます。
amitp

回答:


16

上記のコメントで述べたように、コードを複雑にする前にこれをプロファイルすることをお勧めします。簡単なforループサミングサイコロは、複雑な数式やテーブル作成/検索よりも理解と修正がはるかに簡単です。重要な問題を解決していることを確認するために、常に最初にプロファイルを作成してください。;)

とはいえ、洗練された確率分布を一気にサンプリングするには、主に2つの方法があります。


1.累積確率分布

単一の一様ランダム入力のみを使用して、連続確率分布からサンプリングする巧妙なトリックがあります。それは関係していた累積分布関数をその「値になっていない確率は何の答えも大きく xよりは?」

この関数は減少せず、0から始まり、そのドメインで1に上昇します。2つの6面ダイスの合計の例を以下に示します。

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.エイリアスメソッド

累積分布法は、累積分布関数の逆関数を単純な数式で表現できる場合にうまく機能しますが、それは必ずしも簡単ではなく、可能性さえありません。離散分布の信頼できる代替手段は、エイリアスメソッドと呼ばれるものです。

これにより、2つの独立した均一に分布したランダム入力を使用して、任意の離散確率分布からサンプリングできます。

これは、左下のような分布を取ります(エイリアスの方法では相対的な重みを考慮しているため、面積/重みが1にならないことを心配しないでください)。正しい場所:

  • 結果ごとに1つの列があります。
  • 各列は最大2つの部分に分割され、各部分は元の結果の1つに関連付けられます。
  • 各結果の相対的な面積/重量は保持されます。

分布をルックアップテーブルに変換するエイリアスメソッドの例

サンプリング方法に関するこの優れた記事の画像に基づく図)

コードでは、各列から代替結果を選択する確率と、その代替結果のアイデンティティ(または「エイリアス」)を表す2つのテーブル(または2つのプロパティを持つオブジェクトのテーブル)でこれを表します。その後、次のように分布からサンプリングできます。

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. 各結果のエントリを持つテーブルのペアを作成します。完全な方法はこの回答の範囲を超えているため、エイリアスメソッドアルゴリズムのこの説明を参照することを強くお勧めします

  3. これらのテーブルを保存し、このディストリビューションから新しいランダムダイスロールが必要になるたびにそれらを参照し直してください。

これは時空のトレードオフです。事前計算のステップはやや網羅的であり、結果の数に比例してメモリを確保する必要があります(1000d6の場合でも、1桁のキロバイトを話しているので、睡眠を失うことはありません)分布がどれほど複雑であっても一定時間です。


これらのメソッドのいずれかが何らかの用途に役立つことを願っています(または、単純なメソッドの単純さはループに要する時間の価値があると確信していること);)


1
素晴らしい答え。私は素朴なアプローチが好きです。エラーの余地がはるかに少なく、わかりやすい。
bummzack

参考までに、この質問はredditに関するランダムな質問からのコピー&ペーストです。
ベイランクール

完成度については、これは @AlexandreVaillancourtが話しているredditスレッドだと思います。そこでの答えは、主にループバージョンを維持すること(その時間コストが合理的である可能性が高いという証拠がある)または正規/ガウス分布を使用して多数のサイコロを近似することを示唆しています。
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);
}

このコードは20回ループし、1〜6の乱数を出力します(両端を含む)。このコードのパフォーマンスについて説明すると、Randomオブジェクトの作成に時間がかかります(作成時のコンピューターの内部クロックに基づいて擬似乱数整数の配列を作成する必要があります)。 nextInt()呼び出しごとのルックアップ。各「ロール」は一定時間の操作であるため、これにより、ローリングが時間的に非常に安くなります。また、minからmaxの範囲は重要ではないことに注意してください(つまり、コンピューターがd10000をロールするのと同じくらい簡単にd6をロールするのは簡単です)。時間の複雑さに関して言えば、ソリューションのパフォーマンスは単純にO(n)です。ここで、nはサイコロの数です。

あるいは、1つのd100(またはその場合はd10000)ロールで任意の数のd6ロールを概算できます。この方法を使用して、最初にs [サイコロの面の数] * n [サイコロの数]パーセンテージを計算する必要があります(技術的にはs * n-n + 1パーセンテージであり、それを大まかに分割できるはずです)対称であるため、半分になります.2d6ロールをシミュレートする例では、11パーセントを計算し、6が一意であることに注意してください)。ローリング後、バイナリ検索を使用して、ロールがどの範囲に入ったかを把握できます。時間の複雑さの観点から、このソリューションはO(s * n)ソリューションに評価されます。ここで、sは辺の数、nはサイコロの数です。ご覧のとおり、これは前の段落で提案したO(n)ソリューションよりも低速です。

そこから外挿して、1000d20ロールをシミュレートするためにこれらのプログラムの両方を作成したとしましょう。1つ目は、単純に1,000回ロールします。2番目のプログラムでは、他に何かを行う前に、19,001パーセント(1,000〜20,000の潜在的な範囲)を最初に決定する必要があります。したがって、メモリ検索が浮動小数点演算よりも数倍高価な奇妙なシステムを使用している場合を除き、各ロールにnextInt()呼び出しを使用するのが適切な方法のようです。


2
上記の分析はまったく正しくありません。Alias Methodに従って確率とエイリアステーブルを生成するために事前に時間を確保しておけば、一定の時間(2つの乱数とテーブルルックアップ)で任意の離散確率分布からサンプリングできます。したがって、テーブルが準備されたら、5ダイスのロールまたは500ダイスのロールをシミュレートするには、同じ量の作業が必要です。これは、すべてのサンプルで多数のサイコロをループするよりも漸近的に高速ですが、必ずしも問題のより良い解決策とはなりません。;)
DMGregory

0

あなたがサイコロの組み合わせを保存したい場合、良いニュースは解決策があるということです、悪いことは私たちのコンピューターがこの種の問題に関して何らかの形で制限されていることです。

良いニュース:

この問題には決定論的なアプローチがあります。

1 /サイコロのグループのすべての組み合わせを計算する

2 /各組み合わせの確率を決定する

3 /サイコロを投げるのではなく、このリストで結果を検索する

悪いニュース:

繰り返しのある組み合わせの数は、次の式で与えられます

Γnk=n+k1k=n+k1k n1

フランス語のウィキペディアから):

繰り返しとの組み合わせ

つまり、たとえば、サイコロが150個の場合、698'526'906の組み合わせがあります。確率を32ビットの浮動小数点数として保存すると、2,6 GBのメモリが必要になりますが、インデックスのメモリ要件を追加する必要があります...

計算用語では、コンボリューションによって組み合わせ数を計算できます。これは便利ですが、メモリの制約を解決しません。

結論として、多数のサイコロについては、各組み合わせに関連する確率を事前計算するのではなく、サイコロを投げて結果を観察することをお勧めします。

編集

ただし、サイコロの合計にのみ関心があるので、はるかに少ないリソースで確率を保存できます。

畳み込みを使用して、サイコロの合計ごとに正確な確率を計算できます。

一般式は Fm=nF1nF1mn

次に、1つのサイコロを使用して各結果の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.