私はRandom (Java.util.Random)
を使って52枚のカードのデッキをシャッフルしました。 52あります! (8.0658175e + 67)の可能性それでも、Java.util.Random
のシードはlong
であり、これは2 ^ 64(1.8446744e + 19)とはるかに小さいことがわかりました。
ここから、Java.util.Random
が本当にそれほどランダムかどうかは疑わしいです ;それは実際にすべて52を生成することができますか?可能性は?
そうでなければ、どうすれば確実に52個すべてを生成することができる、より優れたランダムシーケンスを生成できます。可能性は?
ランダムな順列を選択するには、あなたの質問が意味するものよりも多くのランダム性を同時に必要とします。説明させてください。
悪いニュース:より多くのランダム性が必要です。
あなたのアプローチの基本的な欠陥は、〜2の間で選択しようとしていることです226 64ビットのエントロピー(ランダムシード)を使用した可能性。 〜2の間で公平に選択するには226 64の代わりに226ビットのエントロピーを生成する方法を見つけなければならない可能性があります。
ランダムビットを生成する方法はいくつかあります: 専用ハードウェア 、 CPU命令 、 OSインターフェイス 、 オンラインサービス 。あなたの質問にはすでに何らかの方法で64ビットを生成できるという暗黙の仮定が既にあるので、4回だけあなたがやろうとしていたことを何でもして、余分なビットを慈善団体に寄付してください。 :)
朗報:ランダム性が少なくて済む。
これらの226個のランダムビットを取得したら、残りを確定的に行うことができるため、Java.util.Random
のプロパティを無関係にすることができます。方法は次のとおりです。
52個すべてを生成するとしましょう。順列(私に耐える)とそれらを辞書的に並べ替えます。
順列の1つを選択するために必要なのは、0
と52!-1
の間の単一のランダムな整数です。その整数は、226ビットのエントロピーです。並べ替えられた順列リストのインデックスとして使用します。ランダムインデックスが均一に分布している場合、すべての順列が選択できることが保証されるだけでなく、それらは同等に選択されます(質問が尋ねているものよりも強力な保証です) 。
今、あなたは実際にそれらのすべての順列を生成する必要はありません。仮想ソートリストでランダムに選択された位置を指定すると、1つを直接生成できます。これはO(n2) レーマーを使用して時間[1] コード ( 番号順列 および 階乗数システム も参照)。ここでのnは、デッキのサイズ、つまり52です。
これにはC実装があります StackOverflow answer 。そこにはn = 52でオーバーフローする整数変数がいくつかありますが、幸運にもJavaではJava.math.BigInteger
を使用できます。残りの計算は、ほぼそのまま転写できます。
public static int[] shuffle(int n, BigInteger random_index) {
int[] perm = new int[n];
BigInteger[] fact = new BigInteger[n];
fact[0] = BigInteger.ONE;
for (int k = 1; k < n; ++k) {
fact[k] = fact[k - 1].multiply(BigInteger.valueOf(k));
}
// compute factorial code
for (int k = 0; k < n; ++k) {
BigInteger[] divmod = random_index.divideAndRemainder(fact[n - 1 - k]);
perm[k] = divmod[0].intValue();
random_index = divmod[1];
}
// readjust values to obtain the permutation
// start from the end and check if preceding values are lower
for (int k = n - 1; k > 0; --k) {
for (int j = k - 1; j >= 0; --j) {
if (perm[j] <= perm[k]) {
perm[k]++;
}
}
}
return perm;
}
public static void main (String[] args) {
System.out.printf("%s\n", Arrays.toString(
shuffle(52, new BigInteger(
"7890123456789012345678901234567890123456789012345678901234567890"))));
}
[1] Lehrer と混同しないでください。 :)
あなたの分析は正しいです:どんな特定の種でも擬似乱数生成器を種にすることはあなたが得ることができる順列の数を2に制限して、シャッフルの後に同じシーケンスを生み出さなければなりません64。このアサーションは 実験的に検証するのが簡単Collection.shuffle
を2回呼び出し、同じシードで初期化されたRandom
オブジェクトを渡して、2つのランダムシャッフルが同一であることを確認することで確認できます。
そのための解決策は、より大きなシードを可能にする乱数ジェネレータを使用することです。 Javaは、事実上無制限のサイズのbyte[]
配列で初期化できる SecureRandom
classを提供します。タスクを完了するためにSecureRandom
のインスタンスをCollections.shuffle
に渡すことができます。
byte seed[] = new byte[...];
Random rnd = new SecureRandom(seed);
Collections.shuffle(deck, rnd);
一般に、擬似乱数生成器(PRNG)は、その状態の長さが226ビット未満の場合、52項目のリストのすべての置換の中から選択することはできません。
Java.util.Random
は2の法でアルゴリズムを実装します48;したがって、その状態の長さは48ビットに過ぎず、私が参照した226ビットよりはるかに短いです。より大きなステート長を持つ別のPRNGを使用する必要があります。具体的には、52階乗以上の期間を持つものです。
乱数発生器に関する私の 記事の "シャッフル"も参照してください 。
この考慮はPRNGの性質とは無関係です。暗号化と非暗号化の両方のPRNGに同じように適用されます(もちろん、非暗号化PRNGは情報セキュリティが関係している場合は常に不適切です)。
Java.security.SecureRandom
は無制限の長さのシードを渡すことを可能にしますが、SecureRandom
実装は基礎となるPRNG(例えば "SHA1PRNG"や "DRBG")を使うことができます。そしてそれが52の階乗順列の中から選択できるかどうかは、そのPRNGの期間(そしてより少ない程度ではあるが状態の長さ)に依存します。 ( 私は "状態の長さ" を "PRNGの状態を初期化するために取ることができるシードの最大サイズ そのシードを短くしたり圧縮したりすることなく と定義する)。
これを理解するのは少し難しいので、事前に謝罪します...
まず、Java.util.Random
が完全にランダムではないことをすでに知っています。シードから完全に予測可能な方法でシーケンスを生成します。シードは64ビット長しかないため、2 ^ 64の異なるシーケンスしか生成できないことは完全に正しいです。何らかの方法で64個の実際のランダムビットを生成し、それらを使用してシードを選択した場合、そのシードを使用して、52のallからランダムに選択することはできません。等しい確率で可能なシーケンス。
ただし、この事実は結果なしは、実際に2 ^ 64を超えるシーケンスを生成しない限り、2について「特別な」または「顕著に特別な」ものがない限りcan生成する^ 64シーケンス。
1000ビットのシードを使用したはるかに優れたPRNGがあるとしましょう。初期化する方法は2つあると想像してください。1つはシード全体を使用して初期化する方法、もう1つはシードを初期化する前に64ビットにハッシュする方法です。
どの初期化子がどれであるかわからなかった場合、それらを区別するためのテストを作成できますか? same 64ビットを2回使用して、悪いものを初期化することができる(不幸な)幸運でない限り、答えはノーです。特定のPRNG実装の弱点についての詳細な知識がなければ、2つの初期化子を区別できませんでした。
あるいは、Random
クラスに2 ^ 64シーケンスの配列があり、遠い過去のある時点で完全にランダムに選択され、シードはこの配列の単なるインデックスであったと想像してください。
したがって、Random
がシードに64ビットのみを使用するという事実は、同じシードを2回使用する有意な機会がない限り、実際にはnot統計的に問題になります。
もちろん、cryptographicの目的では、64ビットのシードだけでは十分ではありません。システムが同じシードを2回使用することは計算上実行可能であるためです。
編集:
上記のすべてが正しいとしても、Java.util.Random
の実際の実装は素晴らしいものではないことを付け加えます。カードゲームを作成している場合は、MessageDigest
APIを使用して"MyGameName"+System.currentTimeMillis()
のSHA-256ハッシュを生成し、それらのビットを使用してデッキをシャッフルします。上記の議論により、ユーザーが実際にギャンブルでない限り、currentTimeMillis
がlongを返すことを心配する必要はありません。ユーザーがare本当にギャンブルしている場合は、シードなしでSecureRandom
を使用します。
私はこれに少し違うタックを取ります。あなたはあなたの仮定に正しいです - あなたのPRNGは52をすべてヒットすることができないでしょう!可能性.
問題は次のとおりです。あなたのカードゲームの規模は?
シンプルなクロンダイクスタイルのゲームを作っているのですか? それならあなたは絶対に52人全員必要ではありません!可能性代わりに、このように見てください。プレイヤーは18のquintillion個の異なるゲームを持つことになります。 「誕生日の問題」を考慮しても、彼らは最初の複製ゲームに遭遇する前に何十億もの手をプレイしなければならなかったでしょう。
モンテカルロシミュレーションをしているのですか? それならあなたはおそらく大丈夫です。 PRNGの 'P'が原因でアーティファクトを処理する必要があるかもしれませんが、シードスペースが小さいことが原因で問題に遭遇することはおそらくないでしょう(ここでも、独自の可能性の五分の一を検討しています)。反対に、反復回数が多い場合は、低シードスペースが問題になる可能性があります。
もし多人数参加型のカードゲームをしているのなら、特にお金があるのなら? それからあなたはオンラインポーカーサイトがあなたが頼んでいるのと同じ問題をどのように扱ったかについていくつかのグーグルをする必要があるだろう。低シードスペースの問題は平均的なプレイヤーにとっては注目に値するではありませんが、時間の投資に見合う価値があるのであれば悪用可能なです。 (全てのポーカーサイトはPRNGがハッキングされた段階を経て、他のプレイヤー全員のホールカードを見ることができるようになりました。露出したカードから種を推測するだけです。)don'tは、単により良いPRNGを見つける - あなたはそれをCrypto問題と同じくらい真剣に扱う必要があるだろう。
Dasblinkenlightと基本的に同じ短い解決策:
// Java 7
SecureRandom random = new SecureRandom();
// Java 8
SecureRandom random = SecureRandom.getInstanceStrong();
Collections.shuffle(deck, random);
あなたは内部の状態を心配する必要はありません。長い説明理由:
このようにしてSecureRandom
インスタンスを作成すると、OS固有の真の乱数ジェネレータにアクセスします。これは、ランダムビットを含む値がアクセスされるエントロピープール(例えば、ナノ秒タイマの場合、ナノ秒の精度は本質的にランダムである)または内部ハードウェア数生成器のいずれかである。
依然として偽のトレースを含む可能性があるこの入力(!)は、それらのトレースを削除する暗号学的に強力なハッシュに入れられます。それが、それらの番号を作成するためではなく、それらのCSPRNGが使用される理由です。 SecureRandom
は使用されたビット数(getBytes()
、getLong()
など)を追跡するカウンタを持ち、 必要に応じてSecureRandom
をエントロピービットで補充します 。
手短に言うと、異議を忘れて、真の乱数発生器としてSecureRandom
を使用するだけです。
もしあなたがこの数を単なるビット(またはバイト)の配列であると考えるならば、多分あなたはこの スタックオーバーフロー で提案された(安全な)Random.nextBytes
の解決法を使い、そしてその配列をnew BigInteger(byte[])
にマップすることができます。
非常に単純なアルゴリズムは、SHA-256を0から上に向かって増加する一連の整数に適用することです。 SHA-256の出力が0と2の間の一様分布の整数と同じくらい良いと仮定した場合256 - 1それで我々はそのタスクに対して十分なエントロピーを持っている。
SHA256の出力(整数で表現されるとき)から順列を得るためには、この疑似コードのように、単にモジュロ52、51、50 ...を減らす必要があります。
deck = [0..52]
shuffled = []
r = SHA256(i)
while deck.size > 0:
pick = r % deck.size
r = floor(r / deck.size)
shuffled.append(deck[pick])
delete deck[pick]