我一直使用Random (java.util.Random)
来洗牌一副有52张牌的扑克牌。总共有52!(8.0658175e+67)种可能性。但是,我发现java.util.Random
的种子是一个long
,比较小,只有2^64 (1.8446744e+19)。
基于这个,我怀疑java.util.Random
是否真的那么随机;它是否能够生成所有的52!种可能性?
如果不能,我该如何可靠地生成一个更好的随机序列以产生所有52!种可能性?
我一直使用Random (java.util.Random)
来洗牌一副有52张牌的扑克牌。总共有52!(8.0658175e+67)种可能性。但是,我发现java.util.Random
的种子是一个long
,比较小,只有2^64 (1.8446744e+19)。
基于这个,我怀疑java.util.Random
是否真的那么随机;它是否能够生成所有的52!种可能性?
如果不能,我该如何可靠地生成一个更好的随机序列以产生所有52!种可能性?
java.util.Random
的属性可以被忽略。以下是如何实现的。0
和52!-1
之间的随机整数。该整数是我们的226位熵。我们将使用它作为索引进入已排序的排列列表。如果随机索引均匀分布,则不仅保证可以选择所有排列,而且它们将被等概率地选择(这比问题所要求的更强)。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混淆。 :)
您的分析是正确的:使用特定种子初始化伪随机数生成器必须在洗牌后产生相同的序列,这将限制您可获得的排列数量为264。通过调用两次 Collection.shuffle
并传递一个使用相同种子初始化的 Random
对象,并观察两次随机洗牌的结果相同,可以通过实验轻松验证此断言。(点此查看实验)
解决方法是使用允许更大种子的随机数生成器。Java提供了SecureRandom
类,该类可以使用虚拟无限大小的byte[]
数组进行初始化。然后,您可以将SecureRandom
实例传递给Collections.shuffle
以完成任务:
byte seed[] = new byte[...];
Random rnd = new SecureRandom(seed);
Collections.shuffle(deck, rnd);
java.security.SecureRandom
允许传入几乎无限长度的种子,但SecureRandom
的实现可以使用底层的伪随机数生成器(例如"SHA1PRNG"或"DRBG")。而且,它取决于底层伪随机数生成器所允许的种子数量,是否能够从52的阶乘排列中进行选择,除非例如,该伪随机数生成器使用与应用程序不透明的独立随机性重新种子化。提前道歉,这可能有点难理解...
首先,您已经知道java.util.Random
并不是完全随机的。它以完全可预测的方式从种子生成序列。您的观点是完全正确的,由于种子只有64位长,它只能生成2^64个不同的序列。如果您以某种方式生成了64位真正的随机数并使用它们来选择种子,您不能使用该种子随机地选择所有52!个可能的序列,使它们具有相等的概率。
然而,只要您不会实际生成超过2^64个序列,只要没有任何“ 特殊的”或“明显特殊的”2^64个序列,这个事实就 没有任何影响。
假设您有一个使用1000位种子的更好的伪随机数生成器(PRNG)。想象一下,您有两种方法来初始化它——一种方法将使用整个种子进行初始化,另一种方法将在初始化之前将种子哈希为64位。
如果您不知道哪个初始化程序是哪个,则可以编写任何测试来区分它们吗?除非您足够(不)幸运,以至于用相同的64位两次初始化不良程序,否则答案是否定的。您不能在没有对特定PRNG实现中某些弱点的详细了解的情况下区分两种初始化程序。
或者说,假设Random
类有一个包含2^64个序列的数组,在很遥远的过去某个时刻完全随机地选择了这些序列,并且种子只是这个数组中的一个索引。
因此,实际上Random
使用仅64位种子在统计上不一定成问题,只要不会有重复使用相同种子的显著机会。
当然,对于密码学目的而言,64位种子就不够用了,因为让系统两次使用相同的种子具有可行性。
编辑:
我应该补充说明,即使以上所有内容都是正确的,但实际上java.util.Random
的实现并不太好。如果你正在编写一款扑克游戏,也许可以使用MessageDigest
API生成"MyGameName"+System.currentTimeMillis()
的SHA-256哈希值,并使用这些位来洗牌。根据上述论点,只要用户不是真正赌博,就不必担心currentTimeMillis
返回长整型。如果你的用户确实在进行真正的赌博,那么请使用无种子的SecureRandom
。
我会采取不同的方法。你的假设是正确的 - 你的伪随机数生成器无法涵盖所有52!种可能性。
问题是:你的纸牌游戏规模有多大?
如果你正在制作一个简单的类似纸牌游戏? 那么你绝对不需要所有52!种可能性。相反,你可以这样看待它:一个玩家将拥有18万亿个独特的游戏。即使考虑到“生日悖论”,他们也必须打数十亿手才能遇到第一个重复的游戏。
如果你正在进行蒙特卡罗模拟? 那么你可能没事。你可能需要处理由于PRNG中的“P”而产生的人为因素,但你可能不会因为低种子空间而遇到问题(再次强调,你在寻找百万亿个独特的可能性)。另一方面,如果你要处理大量迭代次数,那么,是的,你的低种子空间可能成为决定因素。
如果你正在制作多人卡牌游戏,特别是涉及到赌金的话? 那么你需要在谷歌上搜索一些关于在线扑克网站如何处理你所询问的同样问题的信息。因为虽然低种子空间问题对于普通玩家而言不是很明显,但如果它值得投入时间来利用,那么它是可以被利用的。(扑克网站都经历过一个阶段,他们的 PRNG 被 "黑客" 破解了,从暴露的牌中推算种子,让某个人可以看到所有其他玩家的底牌)。如果你处于这种情况,不要简单地找一个更好的 PRNG - 你需要像处理加密问题一样认真对待它。
这是与dasblinkenlight本质相同的简短解决方案:
// Java 7
SecureRandom random = new SecureRandom();
// Java 8
SecureRandom random = SecureRandom.getInstanceStrong();
Collections.shuffle(deck, random);
您不需要担心内部状态。以下是长说明:
当您以这种方式创建SecureRandom
实例时,它会访问特定于操作系统的真随机数生成器。这可能是一个熵池,其中包含随机位(例如,对于纳秒计时器,纳秒精度基本上是随机的),或者是内部硬件数字生成器。
这个输入(!)可能仍然包含杂散的痕迹,被馈入一个密码强度的哈希函数中,该函数会去除这些痕迹。这就是为什么使用这些CSPRNGs,而不是为了创建这些数字本身!SecureRandom
有一个计数器,跟踪使用了多少位(getBytes()
,getLong()
等),并且在必要时重新填充SecureRandom
的熵位。
简而言之:只需忘记异议,将SecureRandom
用作真正的随机数生成器即可。
Random.nextBytes
解决方案,然后将该数组映射到new BigInteger(byte[])
。请注意保留HTML标签。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]
Random
函数生成的数字并非真正的随机数,而是伪随机数。其中的 P 代表“伪”。如果需要真正的随机数,你需要使用随机数来源(例如 random.org)。 - T.J. Crowder