なぜ乱数ジェネレーターを使用するときにモジュロバイアスがあると人々は言うのですか?


277

私はこの質問が頻繁に尋ねられるのを見ましたが、それに対する真の具体的な答えを見たことはありません。そこでrand()、C ++のように乱数ジェネレータを使用するときに「モジュロバイアス」が正確に存在する理由を人々が理解できるように役立つものをここに投稿します。

回答:


394

で定義されている定数であるrand()0からRAND_MAXまでの自然数を選択する疑似乱数ジェネレーターもありcstdlibます(の一般的な概要については、この記事を参照してくださいrand())。

次に、たとえば0と2の間の乱数を生成したい場合はどうなりますか?説明のために、RAND_MAX10としましょう。私はを呼び出して、0と2の間の乱数を生成することにしrand()%3ます。ただし、rand()%30と2の間の数を等しい確率で生成しません!

rand()0、3、6、または9を返す場合 rand()%3 == 0。したがって、P(0)= 4/11

rand()1、4、7、または10を返す場合 rand()%3 == 1。したがって、P(1)= 4/11

rand()2、5、または8を返す場合 rand()%3 == 2。したがって、P(2)= 3/11

これは、等しい確率で0と2の間の数を生成しません。もちろん、範囲が小さい場合、これは最大の問題ではない可能性がありますが、範囲が大きい場合、分布が歪む可能性があり、小さい数に偏ります。

では、rand()%n等確率で0からn-1までの範囲の数値を返すのはいつでしょうか。ときRAND_MAX%n == n - 1。この場合、以前の仮定rand()は0からRAND_MAX等しい確率で数値を返すため、nのモジュロクラスも均等に分散されます。

では、この問題をどのように解決すればよいでしょうか?おおまかな方法​​は、希望する範囲の数値が得られるまで乱数を生成し続けることです。

int x; 
do {
    x = rand();
} while (x >= n);

それは低い値のために非効率的だnあなたが唯一持っているので、n/RAND_MAXあなたの範囲の値を得ることのチャンスを、あなたが実行する必要がありますので、RAND_MAX/nへの呼び出しをrand()平均で。

より効率的な式のアプローチはn、のようにRAND_MAX - RAND_MAX % nで割り切れる長さのいくつかの大きな範囲を取り、範囲内にあるものを取得するまで乱数を生成し続け、次に係数を取得することです。

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;

の値が小さい場合n、これはめったにへの複数の呼び出しを必要としませんrand()


引用された作品と参考文献:



6
についてのもう1つの考え方は、RAND_MAX%n == n - 1_ _です(RAND_MAX + 1) % n == 0。コードを読むとき% something == 0、他の計算方法よりも「均等に割り切れる」と理解する傾向があります。 もちろん、C ++ stdlibがRAND_MAXと同じ値の場合INT_MAX(RAND_MAX + 1)きっと動作しません。したがって、マークの計算は依然として最も安全な実装です。
Slipp D. Thompson

とてもいい答えです!
Sayali Sonawane 2017

私はつまらないかもしれませんが、無駄なビットを減らすことが目的である場合、RAND_MAX(RM)がNで等しく割り切れる値よりも1だけ小さいエッジ条件でこれを少し改善できます。このシナリオでは、ビットを無駄にする必要はありません。 X> =(RM-RM%N))を実行します。これは、Nの小さな値に対してはほとんど価値がありませんが、Nの大きな値に対しては大きな値になります。SlippD. Thompsonが述べたように、のみ機能する解決策がありますINT_MAX(IM)> RAND_MAXの場合。ただし、等しい場合は中断します。:次のように-しかし、我々は計算X> =(RM%N RM)を修正することができ、このための簡単な解決策がある
ベンPersonick

X> = RM-((((RM%N)+ 1)%N)
Ben Personick 2017年

問題を詳細に説明し、コード例のソリューションを提供する追加の回答を投稿しました。
Ben Personick 2017年

36

ランダムを選択し続けることは、バイアスを取り除くための良い方法です。

更新

で割り切れる範囲のxを検索すると、コードを高速にすることができnます。

// Assumptions
// rand() in [0, RAND_MAX]
// n in (0, RAND_MAX]

int x; 

// Keep searching for an x in a range divisible by n 
do {
    x = rand();
} while (x >= RAND_MAX - (RAND_MAX % n)) 

x %= n;

上記のループは非常に高速でなければなりません。たとえば、平均で1回の繰り返しです。


2
Yuck :-Pをdoubleに変換してからMAX_UPPER_LIMIT / RAND_MAXを掛けると、はるかにクリーンでパフォーマンスが向上します。
ボイシー2012年

22
@boycy:ポイントを逃しました。rand()返すことができる値の数がの倍数でない場合、n何をしても、それらの値の一部を破棄しない限り、必然的に「モジュロバイアス」が発生します。user1413793はそれをうまく説明しています(ただし、その回答で提案されているソリューションは本当に不幸です)。
TonyK

4
@TonyK謝罪、要点を逃した。十分に考えていなかったため、バイアスは明示的なモジュラス演算を使用するメソッドにのみ適用されると考えました。私を修正してくれてありがとう:-)
ボイシー

演算子の優先順位はRAND_MAX+1 - (RAND_MAX+1) % n正しく機能しますがRAND_MAX+1 - ((RAND_MAX+1) % n)、明確にするために記述する必要があります。
Linus Arver

4
これはRAND_MAX == INT_MAX (ほとんどのシステムでそうであるように)動作しません。上記の@ user1413793への2番目のコメントを参照してください。
BlueRaja-Danny Pflughoeft

19

@ user1413793は問題について正しいです。1つのポイントを除いて、それについてさらに説明するつもりはありません。そうです。の小さな値nと大きな値のRAND_MAX場合、モジュロバイアスは非常に小さくなります。ただし、バイアスを誘発するパターンを使用するということは、乱数を計算するたびにバイアスを考慮し、ケースごとに異なるパターンを選択する必要があることを意味します。そして、あなたが間違った選択をした場合、それがもたらすバグは微妙であり、ユニットテストすることはほとんど不可能です。適切なツール(などarc4random_uniform)のみを使用する場合と比較すると、これは追加の作業であり、作業量が少なくなるわけではありません。より多くの作業を行い、より悪い解決策を得ることはひどいエンジニアリングです。特に、ほとんどのプラットフォームで毎回正しく行うことが簡単な場合はなおさらです。

残念ながら、ソリューションの実装はすべて正しくないか、本来よりも効率が低くなっています。(各ソリューションには問題を説明するさまざまなコメントがありますが、それらを解決するために修正されたソリューションはありません。)これはカジュアルな回答を求める人を混乱させる可能性があるため、ここでは既知の良好な実装を提供します。

繰り返しになりますが、最善のソリューションはarc4random_uniform、それを提供するプラットフォーム、またはプラットフォーム(Random.nextIntJava など)に類似した範囲ソリューションで使用することです。それはあなたにコードコストなしで正しいことをします。ほとんどの場合、これは正しい呼び出しです。

がない場合はarc4random_uniform、オープンソースの機能を使用して、より広範囲のRNG上でどのように実装されているかを正確に確認できます(ar4randomこの場合、同様のアプローチが他のRNG上でも機能する可能性があります)。

ここでOpenBSDの実装は

/*
 * Calculate a uniformly distributed random number less than upper_bound
 * avoiding "modulo bias".
 *
 * Uniformity is achieved by generating new random numbers until the one
 * returned is outside the range [0, 2**32 % upper_bound).  This
 * guarantees the selected random number will be inside
 * [2**32 % upper_bound, 2**32) which maps back to [0, upper_bound)
 * after reduction modulo upper_bound.
 */
u_int32_t
arc4random_uniform(u_int32_t upper_bound)
{
    u_int32_t r, min;

    if (upper_bound < 2)
        return 0;

    /* 2**32 % x == (2**32 - x) % x */
    min = -upper_bound % upper_bound;

    /*
     * This could theoretically loop forever but each retry has
     * p > 0.5 (worst case, usually far better) of selecting a
     * number inside the range we need, so it should rarely need
     * to re-roll.
     */
    for (;;) {
        r = arc4random();
        if (r >= min)
            break;
    }

    return r % upper_bound;
}

同様のことを実装する必要がある人のために、このコードに関する最新のコミットコメントに注目する価値があります。

arc4random_uniform()を2**32 % upper_boundとして計算するように変更し -upper_bound % upper_boundます。コードを簡略化し、ILP32アーキテクチャとLP64アーキテクチャの両方で同じにします。また、64ビット剰余の代わりに32ビット剰余を使用することにより、LP64アーキテクチャではわずかに高速化します。

Jorden Verwerがtech @ ok deraadtで指摘しました。DJMやオットーからの異議なし

Java実装も簡単に見つけることができます(前のリンクを参照)。

public int nextInt(int n) {
   if (n <= 0)
     throw new IllegalArgumentException("n must be positive");

   if ((n & -n) == n)  // i.e., n is a power of 2
     return (int)((n * (long)next(31)) >> 31);

   int bits, val;
   do {
       bits = next(31);
       val = bits % n;
   } while (bits - val + (n-1) < 0);
   return val;
 }

arcfour_random() その実装で実際に実際のRC4アルゴリズムを実際に使用する場合、出力には間違いなくバイアスがあることに注意してください。うまくいけば、ライブラリの作成者が、同じインターフェイスの背後でより優れたCSPRNGを使用するように切り替えたことを確認します。BSDの1つが実際にChaCha20アルゴリズムを使用して実装していることを思い出しますarcfour_random()。より多くのセキュリティや、ビデオポーカーなどの他の重要なアプリケーションのために、それは無用RC4出力バイアス上:blog.cryptographyengineering.com/2013/03/...
rmalayter

2
@rmalayter iOSおよびOS Xでは、arc4randomは、システムで最高品質のエントロピーである/ dev / randomから読み取ります。(名前の「arc4」は歴史的であり、互換性のために保存されています。)
Rob Napier

@Rob_Napierは知っておき/dev/randomますが、過去に一部のプラットフォームでRC4も使用しました(LinuxはカウンターモードでSHA-1を使用します)。残念ながら、検索で見つかったmanページは、RC4が提供するさまざまなプラットフォームでまだ使用されていることを示していますarc4random(実際のコードは異なる場合があります)。
rmalayter

1
よくわかりません。じゃないです-upper_bound % upper_bound == 0か?
Jon McClung

1
@JonMcClung -upper_bound % upper_boundint、32ビットより広い場合、実際には0になります。(それはのBSD-ismであると(u_int32_t)-upper_bound % upper_bound)想定しu_int32_tていますuint32_t)。
イアンアボット

14

定義

モジュロバイアスは、モジュロ演算を使用して出力セットを入力セットのサブセットに削減する際の固有のバイアスです。一般に、出力セットのサイズが入力セットのサイズの約数ではないときにモジュロ演算を使用する場合のように、入力セットと出力セットの間のマッピングが均等に分散されない場合は常にバイアスが存在します。

このバイアスは、数値をビットの文字列(0と1)として表す計算では特に避けがたいものです。真にランダムなランダム性のソースを見つけることも非常に困難ですが、この議論の範囲を超えています。この回答の残りの部分では、真にランダムなビットの無制限のソースが存在すると仮定します。

問題の例

これらのランダムビットを使用して、サイコロ(0〜5)のシミュレーションを検討してみましょう。6つの可能性があるので、6を表すのに十分なビット、つまり3ビットが必要です。残念ながら、3つのランダムなビットは8つの可能な結果を​​もたらします:

000 = 0, 001 = 1, 010 = 2, 011 = 3
100 = 4, 101 = 5, 110 = 6, 111 = 7

私たちは、しかし、このプレゼント、値のモジュロ6を取ることによって、正確に6に結果セットのサイズを小さくすることができモジュロバイアスの問題を:1100が得られ、かつ1111を生み出す。このダイがロードされています。

可能なソリューション

アプローチ0:

理論上、ランダムなビットに頼るのではなく、小さな軍隊を雇って1日中サイコロを振ってデータベースに結果を記録し、各結果を1回だけ使用することができます。これは、それが思うほど実用的であり、とにかく真にランダムな結果をもたらすことはおそらくありません(しゃれた意図)。

アプローチ1:

モジュラスを使用する代わりに、素朴だが数学的に正しい解決策は、生成された結果を破棄110して111、単純に3つの新しいビットで再試行することです。残念ながら、これは、再ロールのそれぞれを含めて、再ロールが必要になる可能性が各ロールに25%あることを意味します。これは、最も些細な使用を除いて、すべて非現実的です。

アプローチ2:

より多くのビットを使用します。3ビットの代わりに4を使用します。これにより、16の結果が得られます。もちろん、結果が5より大きいときはいつでも再ロールすると事態が悪化し(10/16 = 62.5%)、それだけでは効果がありません。

2 * 6 = 12 <16であることに注意してください。したがって、12未満の任意の結果を安全に取得し、その結果を均等に分配するためにそのモジュロ6を減らすことができます。他の4つの結果は破棄する必要があり、前のアプローチと同様に再ロールします。

最初はいいように聞こえますが、数学をチェックしましょう:

4 discarded results / 16 possibilities = 25%

この場合、1ビット追加してもまったく効果がありませんでした

その結果は残念ですが、5ビットでもう一度試してみましょう。

32 % 6 = 2 discarded results; and
2 discarded results / 32 possibilities = 6.25%

明確な改善ですが、多くの実際的なケースでは十分ではありません。朗報ですが、ビットを追加しても、破棄して再ロールする必要が生じる可能性は高くなりません。これはサイコロだけでなく、すべての場合に当てはまります。

ただし、示されているように、1ビットを追加しても何も変更されない場合があります。実際、ロールを6ビットに増やしても、確率は6.25%のままです。

これは2つの追加の質問を頼みます:

  1. 十分なビットを追加した場合、破棄の確率が減少するという保証はありますか?
  2. 一般的な場合、何ビットで十分ですか?

一般的なソリューション

ありがたいことに、最初の質問への答えはイエスです。6の問題は、2 ^ x mod 6が2と4の間で反転し、偶然にも互いに2の倍数であるため、偶数x> 1の場合

[2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1)

したがって、6は規則ではなく例外です。同じ方法で連続する2の累乗を生成するより大きい係数を見つけることは可能ですが、最終的にこれはラップアラウンドする必要があり、破棄の確率は減少します。

さらなる証拠を提供することなく、一般に必要なビット数2倍にすると、廃棄の可能性は小さくなり、通常は重要ではなくなります。

コンセプトの証明

OpenSSLのlibcrypoを使用してランダムなバイトを提供するプログラムの例を次に示します。コンパイルするときは、-lcryptoほとんどの人が利用できるはずのライブラリにリンクしてください。

#include <iostream>
#include <assert.h>
#include <limits>
#include <openssl/rand.h>

volatile uint32_t dummy;
uint64_t discardCount;

uint32_t uniformRandomUint32(uint32_t upperBound)
{
    assert(RAND_status() == 1);
    uint64_t discard = (std::numeric_limits<uint64_t>::max() - upperBound) % upperBound;
    uint64_t randomPool = RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));

    while(randomPool > (std::numeric_limits<uint64_t>::max() - discard)) {
        RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));
        ++discardCount;
    }

    return randomPool % upperBound;
}

int main() {
    discardCount = 0;

    const uint32_t MODULUS = (1ul << 31)-1;
    const uint32_t ROLLS = 10000000;

    for(uint32_t i = 0; i < ROLLS; ++i) {
        dummy = uniformRandomUint32(MODULUS);
    }
    std::cout << "Discard count = " << discardCount << std::endl;
}

ほとんどの条件下で実際に発生する再ロールの数を確認するために、MODULUSROLLS値をいじることをお勧めします。懐疑的な人は、計算された値をファイルに保存して、分布が正常に見えることを確認することもできます。


私は、誰もあなたの均一ランダム実装を盲目的にコピーしていないことを願っています。randomPool = RAND_bytes(...)ラインは常にになりますrandomPool == 1アサーションによるもの。これにより、常に破棄と再ロールが行われます。別の行で宣言したいと思います。その結果、これにより、RNGは1反復ごとに戻ります。
Qix-モニカは2017

明確にrandomPoolするために、アサーションのおかげで常に成功するため1、OpenSSLのドキュメントRAND_bytes()に従って常に評価されRAND_status()ます。
Qix-モニカは2017

9

モジュロの使用には、通常2つの不満があります。

  • 1つはすべてのジェネレーターに有効です。限界の場合に見やすくなります。ジェネレーターのRAND_MAXが2(C標準に準拠していない)であり、値として0または1のみが必要な場合、moduloを使用すると、ジェネレーターが0と2を生成するときの2倍の頻度で0が生成されます1を生成します(ジェネレータが1を生成する場合)。これは、値をドロップしないとすぐに当てはまることに注意してください。ジェネレーターの値から必要な値へのマッピングに関係なく、一方は他方の2倍の頻度で発生します。

  • ある種のジェネレーターは、少なくとも一部のパラメーターについて、他よりも重要でないビットのランダム性が低くなっていますが、残念なことに、これらのパラメーターには他の興味深い特性があります(RAND_MAXを2のべき乗よりも小さくできるなど)。この問題はよく知られており、長い間ライブラリの実装ではおそらく問題を回避できます(たとえば、C標準のサンプルrand()実装はこの種類のジェネレータを使用しますが、下位16ビットを削除します)。それとあなたは運が悪いかもしれません

のようなものを使用して

int alea(int n){ 
 assert (0 < n && n <= RAND_MAX); 
 int partSize = 
      n == RAND_MAX ? 1 : 1 + (RAND_MAX-n)/(n+1); 
 int maxUsefull = partSize * n + (partSize-1); 
 int draw; 
 do { 
   draw = rand(); 
 } while (draw > maxUsefull); 
 return draw/partSize; 
}

0とnの間の乱数を生成すると、両方の問題が回避されます(RAND_MAX == INT_MAXでオーバーフローが回避されます)

ところで、C ++ 11は、還元とrand()以外のジェネレーターに標準的な方法を導入しました。


n == RAND_MAX?1:(RAND_MAX-1)/(n + 1):ここでの考え方は、最初にRAND_MAXを等しいページサイズNに分割し、次にN内の偏差を返すことですが、コードをこれに正確にマッピングできません。
zinking

1
n + 1バケットに分割するRAND_MAX + 1値があるため、ナイーブバージョンは(RAND_MAX + 1)/(n + 1)である必要があります。RAND_MAX + 1の計算時にオーバーフローを回避するために、1 +(RAND_MAX-n)/(n + 1)に変換できます。n + 1の計算時にオーバーフローを回避するために、最初にn == RAND_MAXのケースがチェックされます。
AProgrammer

+さらに、除算を行うと、再生成数に比べてコストがかかるようです。
zinking

4
剰余を取ることと分割することは同じコストを持っています。一部のISAは、常に両方を提供する1つの命令のみを提供しています。数値を再生成するコストは、nとRAND_MAXに依存します。nがRAND_MAXに関して小さい場合、コストが高くなる可能性があります。そして、明らかに、バイアスはアプリケーションにとって重要ではないと判断するかもしれません。私はそれらを避ける方法を与えます。
AProgrammer 2012年

9

マークのソリューション(承認されたソリューション)はほぼ完璧です。

int x;

do {
    x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));

x %= n;

作成25 3月.162016-03-25 23:16

マークアメリー39k21170211

ただし、RAND_MAXRM)がN(Where N=有効な有効な結果の数)の倍数より1少ない任意のシナリオで、1つの有効な結果のセットを破棄するという警告があります。

つまり、「破棄された値の数」(D)がに等しい場合N、それらは実際には有効なセットです(V)無効なセット(I)ではありません)。

これを引き起こすのは、ある時点でマークがとの違いを見失っNRand_Maxしまうことです。

N有効な応答の数が含まれているため、有効なメンバーが正の整数のみで構成されるセットです。(例:セットN= {1, 2, 3, ... n }

Rand_max ただし、これは(目的のために定義された)任意の数の非負の整数を含むセットです。

最も一般的な形式でRand Maxは、ここで定義されているのはすべての有効な結果のセットであり、理論的には負の数や非数値を含めることができます。

したがってRand_Max、「可能な応答」のセットとしてより適切に定義されます。

ただしN、有効な応答のセット内の値のカウントに対して動作するため、特定のケースで定義されている場合でも、Rand_Max含まれている合計数より1少ない値になります。

マークのソリューションを使用すると、次の場合に値が破棄されます:X => RM-RM%N

EG: 

Ran Max Value (RM) = 255
Valid Outcome (N) = 4

When X => 252, Discarded values for X are: 252, 253, 254, 255

So, if Random Value Selected (X) = {252, 253, 254, 255}

Number of discarded Values (I) = RM % N + 1 == N

 IE:

 I = RM % N + 1
 I = 255 % 4 + 1
 I = 3 + 1
 I = 4

   X => ( RM - RM % N )
 255 => (255 - 255 % 4) 
 255 => (255 - 3)
 255 => (252)

 Discard Returns $True

上記の例でわかるように、Xの値(初期関数から取得した乱数)が252、253、254、または255の場合、これらの4つの値が有効な戻り値のセットを構成していても、それを破棄します。

IE:破棄された値のカウント(I)= N(有効な結果の数)の場合、戻り値の有効なセットは元の関数によって破棄されます。

値NとRMの差をDとして説明すると、次のようになります。

D = (RM - N)

次に、Dの値が小さくなると、この方法に起因する不要な再ロールのパーセンテージは、各自然乗算で増加します。(RAND_MAXが素数と等しくない場合、これは有効な問題です)

例えば:

RM=255 , N=2 Then: D = 253, Lost percentage = 0.78125%

RM=255 , N=4 Then: D = 251, Lost percentage = 1.5625%
RM=255 , N=8 Then: D = 247, Lost percentage = 3.125%
RM=255 , N=16 Then: D = 239, Lost percentage = 6.25%
RM=255 , N=32 Then: D = 223, Lost percentage = 12.5%
RM=255 , N=64 Then: D = 191, Lost percentage = 25%
RM=255 , N= 128 Then D = 127, Lost percentage = 50%

必要な再ロールのパーセンテージはNがRMに近づくほど増加するため、コードを実行しているシステムの制約と検索される値に応じて、これは多くの異なる値で有効な問題になる可能性があります。

これを無効にするために、簡単な修正を加えることができます。

 int x;

 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

 x %= n;

これは、モジュラスを使用して最大値を定義することの追加の特殊性を説明する、より一般的なバージョンの式を提供します。

Nの乗法であるRAND_MAXに小さい値を使用する例。

マークオリジナルバージョン:

RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X >= (RAND_MAX - ( RAND_MAX % n ) )
When X >= 2 the value will be discarded, even though the set is valid.

一般化バージョン1:

RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X > (RAND_MAX - ( ( RAND_MAX % n  ) + 1 ) % n )
When X > 3 the value would be discarded, but this is not a vlue in the set RAND_MAX so there will be no discard.

さらに、NがRAND_MAXの値の数である必要がある場合。この場合、RAND_MAX = INT_MAXでない限り、N = RAND_MAX +1を設定できます。

ループに関しては、N = 1を使用するだけで、Xの任意の値が受け入れられ、最終的な乗数としてIFステートメントが挿入されます。しかし、おそらく、n = 1で関数が呼び出されたときに1を返す正当な理由があるコードがあるかもしれません...

したがって、n = RAND_MAX + 1にしたい場合は、通常はDiv 0エラーを提供する0を使用する方がよい場合があります。

一般化バージョン2:

int x;

if n != 0 {
    do {
        x = rand();
    } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );

    x %= n;
} else {
    x = rand();
}

これらのソリューションはどちらも、RM + 1がnの積である場合に発生する、不必要に破棄された有効な結果で問題を解決します。

2番目のバージョンは、nをRAND_MAXに含まれる可能な値の合計セットと等しくする必要がある場合のエッジケースシナリオもカバーしています。

両方の変更されたアプローチは同じであり、有効な乱数を提供し、破棄される値を最小限に抑える必要性に対するより一般的な解決策を可能にします。

繰り返します:

マークの例を拡張する基本的な一般的なソリューション:

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

 int x;

 do {
     x = rand();
 } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );

 x %= n;

RAND_MAX + 1 = nの1つの追加シナリオを許可する拡張一般ソリューション:

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x;

if n != 0 {
    do {
        x = rand();
    } while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );

    x %= n;
} else {
    x = rand();
}

一部の言語(特にインタプリタ言語)では、while条件の外で比較操作の計算を行うと、何度も再試行が必要な場合でも1回の計算なので、結果が速くなる場合があります。YMMV!

// Assumes:
//  RAND_MAX is a globally defined constant, returned from the environment.
//  int n; // User input, or externally defined, number of valid choices.

int x; // Resulting random number
int y; // One-time calculation of the compare value for x

if n != 0 {
    y = RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) 
    do {
        x = rand();
    } while (x > y);

    x %= n;
} else {
    x = rand();
}

Markのソリューションの問題は、RAND_MAXとnが実際には2つの異なることを意味する場合、RAND_MAXとnを同じ「測定単位」として扱うことだと言っても安全ではありませんか?nは結果の「可能性の数」を表しますが、RAND_MAXは元の可能性の最大値のみを表します。RAND_MAX+ 1は元の可能性の数になります。私は、彼が認めているように見えたので、彼はあなたの結論に取得していない驚いnと、RAND_MAXは式と同じものではありませんでしたよ:RAND_MAX%n = n - 1
ダニーロ・ソウザモラエス

@DaniloSouzaMorãesDaniloありがとうございます。あなたは問題を非常に簡潔に述べています。私は彼が何をどのようにしてどのようにしていたのかを説明するために行きましたが、方法と方法のロジックの詳細に巻き込まれているので、彼が間違って何をしていたかを雄弁に述べることができたとは思いません。なぜ問題があるのか​​、何が問題なのか明確に述べていません。回答を修正して、ここで書いた内容の一部を、受け入れられたソリューションが何をどこで実行しているのかという問題についての自分の要約として使用してもよろしいですか?
Ben Personick

それは素晴らしいでしょう。それのために行く
ダニーロ・ソウザモラエス

1

RAND_MAX3は偏りがあることをこれらの計算から、理にかなっている(実際にはそれよりもはるかに高くなるはずですが、バイアスは依然として存在することになります)。

1 % 2 = 1 2 % 2 = 0 3 % 2 = 1 random_between(1, 3) % 2 = more likely a 1

この場合、との% 2間の乱数が必要な場合は、を実行しないでください。あなたは間の乱数を得ることができますし、実行しているため、この場合には、しかし:の倍数です。0102% 3RAND_MAX3

別の方法

そこはるかに簡単ですが、他の回答に追加するには、ここの間の乱数を得るために私の解決策である0n - 1、そのn偏りなく、さまざまな可能性。

  • 可能性の数をエンコードするために必要なビット数(バイトではない)は、必要なランダムデータのビット数です。
  • ランダムなビットから数をエンコードする
  • この数がの場合>= n、再起動します(モジュロなし)。

本当にランダムなデータを取得するのは簡単ではないので、なぜ必要以上のビットを使用するのか。

以下は、Smalltalkの例で、疑似乱数ジェネレータからのビットのキャッシュを使用しています。私はセキュリティの専門家ではないので、自己責任で使用してください。

next: n

    | bitSize r from to |
    n < 0 ifTrue: [^0 - (self next: 0 - n)].
    n = 0 ifTrue: [^nil].
    n = 1 ifTrue: [^0].
    cache isNil ifTrue: [cache := OrderedCollection new].
    cache size < (self randmax highBit) ifTrue: [
        Security.DSSRandom default next asByteArray do: [ :byte |
            (1 to: 8) do: [ :i |    cache add: (byte bitAt: i)]
        ]
    ].
    r := 0.
    bitSize := n highBit.
    to := cache size.
    from := to - bitSize + 1.
    (from to: to) do: [ :i |
        r := r bitAt: i - from + 1 put: (cache at: i)
    ].
    cache removeFrom: from to: to.
    r >= n ifTrue: [^self next: n].
    ^r

-1

受け入れられた答えは示し、「モジュロバイアス」の低い値にそのルーツを持っていますRAND_MAX。彼は非常に小さい値RAND_MAX(10)を使用して、RAND_MAXが10の場合、%を使用して0と2の間の数値を生成しようとすると、次の結果が得られることを示しています。

rand() % 3   // if RAND_MAX were only 10, gives
output of rand()   |   rand()%3
0                  |   0
1                  |   1
2                  |   2
3                  |   0
4                  |   1
5                  |   2
6                  |   0
7                  |   1
8                  |   2
9                  |   0

したがって、0の4つの出力(4/10の確率)と、1と2の3つの出力(それぞれ3/10の確率)しかありません。

そのため、偏っています。数値が小さいほど、出てくる可能性が高くなります。

しかし、それRAND_MAXはが小さい場合にのみ明らかに表示されます。より具体的には、モッドする数がと比較して大きい場合RAND_MAX

ループよりもはるかに優れたソリューション(めちゃくちゃ非効率的であり、推奨もされません)は、出力範囲がはるかに大きいPRNGを使用することです。メルセンヌツイスターアルゴリズムは4,294,967,295の最大出力を持っています。そのようなものがやったようMersenneTwister::genrand_int32() % 10すべての意図や目的のために、均等に配分され、モジュロバイアス効果は、すべてが、消えてしまいます。


3
あなたの方がより効率的であり、RAND_MAXが大幅に大きい場合は、修正する数値よりも大きくなりますが、それでもバイアスがかかります。とにかく、これらはすべて疑似乱数ジェネレーターであり、それ自体は別のトピックですが、完全な乱数ジェネレーターを想定している場合でも、より低い値にバイアスをかけることができます。
user1413793 2013

最高値は奇数なので、MT::genrand_int32()%20(50 + 2.3e-8)%の時間と1(50-2.3e-8)%の時間を選択します。カジノのRGN(おそらくより広い範囲のRGNを使用する可能性があります)を構築しているのでない限り、どのユーザーも2.3e-8%の時間に余分に気付くことはありません。あなたはここで問題を起こすには小さすぎる数について話している。
bobobobo 2013

7
ループは最良のソリューションです。「めちゃくちゃ非効率」ではありません。最悪の平均ケースでは2回未満の反復が必要です。高いRAND_MAX値を使用すると、モジュロバイアスは減少しますが、除去されません。ルーピングします。
Jared Nielsen

5
RAND_MAXが修正する数値よりも十分に大きい場合、乱数を再生成する必要がある回数は非常に少なく、効率には影響しません。受け入れられた回答で提案されたnものだけでなく、の最大の倍数に対してテストしている限り、ループを続けると言いnます。
Mark Ransom 2015

-3

私は、フォンノイマンのバイアスなしコインフリップ法のコードを書いたところです。これにより、乱数生成プロセスのバイアスが理論的に排除されます。詳細については、(http://en.wikipedia.org/wiki/Fair_coin)を参照してください。

int unbiased_random_bit() {    
    int x1, x2, prev;
    prev = 2;
    x1 = rand() % 2;
    x2 = rand() % 2;

    for (;; x1 = rand() % 2, x2 = rand() % 2)
    {
        if (x1 ^ x2)      // 01 -> 1, or 10 -> 0.
        {
            return x2;        
        }
        else if (x1 & x2)
        {
            if (!prev)    // 0011
                return 1;
            else
                prev = 1; // 1111 -> continue, bias unresolved
        }
        else
        {
            if (prev == 1)// 1100
                return 0;
            else          // 0000 -> continue, bias unresolved
                prev = 0;
        }
    }
}

これはモジュロバイアスには対応していません。このプロセスは、ビットストリームのバイアスを排除するために使用できます。ただし、ビットストリームから0からnまでの均等な分布に到達するには、nが2の累乗よりも1小さくない場合、モジュロバイアスに対処する必要があります。したがって、このソリューションでは、乱数生成プロセスのバイアスを
Rick

2
@リックうーん。たとえば、1から100までの乱数を生成するときにモジュロバイアスを排除するフォンノイマンの方法の論理的な拡張は、次のとおりです。A)rand() % 100100回呼び出す。B)すべての結果が異なる場合は、最初の結果を取ります。C)それ以外の場合、GOTO A.これは機能しますが、予測される反復回数は約10 ^ 42なので、非常に辛抱する必要があります。そして不滅。
Mark Amery 2016年

@MarkAmery確かに動作するはずです。正しく実装されていませんが、このアルゴリズムを調べてください。最初の他には次のようになりますelse if(prev==2) prev= x1; else { if(prev!=x1) return prev; prev=2;}
リック・
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.