我一直在Random (java.util.Random)
洗牌52张牌。有52个!(8.0658175e + 67)的可能性。但是,我发现for的种子java.util.Random
是a long
,其值小得多,为2 ^ 64(1.8446744e + 19)。
从这里开始,我怀疑是否java.util.Random
真的那么随机;它真的有能力产生全部52个!可能性?
如果没有,我怎么能可靠地产生一个可以产生全部52个更好的随机序列!可能性?
我一直在Random (java.util.Random)
洗牌52张牌。有52个!(8.0658175e + 67)的可能性。但是,我发现for的种子java.util.Random
是a long
,其值小得多,为2 ^ 64(1.8446744e + 19)。
从这里开始,我怀疑是否java.util.Random
真的那么随机;它真的有能力产生全部52个!可能性?
如果没有,我怎么能可靠地产生一个可以产生全部52个更好的随机序列!可能性?
Answers:
选择一个随机排列同时需要比您的问题所暗示的更多或更少的随机性。让我解释。
坏消息:需要更多的随机性。
您的方法的根本缺陷在于,它试图使用64位熵(随机种子)在〜2 226种可能性之间进行选择。要在〜2 226种可能性之间进行合理选择,您将必须找到一种方法来生成226位(而不是64位)熵。
有几种产生随机位的方法:专用硬件,CPU指令,OS接口,在线服务。您的问题中已经存在一个隐含的假设,即您可以某种方式生成64位,因此只需将您打算做的事情做四次,然后将多余的位捐赠给慈善机构。:)
好消息:需要更少的随机性。
一旦有了这226个随机位,其余的就可以确定地完成,因此的属性java.util.Random
就可以变得无关紧要了。这是怎么回事。
假设我们生成了全部52个!排列(与我同在)并按字典顺序对其进行排序。
要选择一种排列,我们需要的是0
和之间的一个随机整数52!-1
。该整数是我们的226位熵。我们将其用作排列的排序列表的索引。如果随机索引是均匀分布的,不仅可以保证可以选择所有排列,还可以等概率选择它们(这比问题要强得多)。
现在,您实际上不需要生成所有这些排列。给定它在我们假设的排序列表中的随机选择位置,您可以直接生产一个。可以使用Lehmer [1]代码在O(n 2)时间内完成此操作(另请参见编号置换和乘数系统)。这里的n是甲板的大小,即52。
这个StackOverflow答案中有一个C实现。有几个整数变量会在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
Random
因此,解决方案是使用允许更大种子的随机数生成器。Java提供SecureRandom
了可以用byte[]
几乎无限大小的数组初始化的类。然后,您可以通过一个实例SecureRandom
来Collections.shuffle
完成任务:
byte seed[] = new byte[...];
Random rnd = new SecureRandom(seed);
Collections.shuffle(deck, rnd);
SecureRandom
实现几乎肯定会使用基础PRNG。而且它是否可以从52个阶乘置换中进行选择取决于PRNG的周期(在较小程度上是状态长度)。(请注意,文档说该SecureRandom
实现“至少符合”某些统计测试,并且生成的“必须具有强大的加密能力”,但对底层PRNG的状态长度或周期没有明确的下限。)
通常,如果伪随机数生成器(PRNG)的状态长度小于226位,则不能从它的所有排列中进行选择。
java.util.Random
实现模数为2 48的算法; 因此它的状态长度只有48位,比我提到的226位要少得多。您将需要使用另一种状态长度更大的PRNG,尤其是周期为52阶乘或更大的PRNG。
另请参阅我关于随机数生成器的文章中的 “混洗” 。
这种考虑独立于PRNG的性质;它同样适用于加密和非加密PRNG(当然,无论涉及到信息安全性,非加密PRNG都是不合适的)。
尽管java.security.SecureRandom
允许传入长度不受限制的种子,但该SecureRandom
实现可以使用基础PRNG(例如“ SHA1PRNG”或“ DRBG”)。而且它是否可以从52个阶乘置换中进行选择取决于PRNG的周期(在较小程度上是状态长度)。(请注意,我将“状态长度”定义为“ PRNG在不缩短或压缩该种子的情况下可以用来初始化其状态的种子的最大大小”)。
我先向您道歉,因为这有点难以理解...
首先,您已经知道这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生成的SHA-256哈希值"MyGameName"+System.currentTimeMillis()
,然后使用这些位对卡片组进行洗牌。通过上面的论点,只要您的用户不是真的在赌博,您就不必担心currentTimeMillis
返回的时间很长。如果你的用户是真正的赌博,然后用SecureRandom
无种子。
我将对此采取不同的策略。您的假设是正确的-PRNG将无法达到全部52个目标!可能性。
问题是:您的纸牌游戏规模是多少?
如果您要制作一个简单的克朗代克风格的游戏? 那么您绝对不需要全部52!可能性。而是这样看:一个玩家将拥有18 亿个独特游戏。即使考虑到“生日问题”,他们也必须先打数十亿手才能进入第一个重复游戏。
如果要进行蒙特卡罗模拟? 那你可能还好。由于PRNG中的“ P”,您可能不得不处理工件,但可能不会仅由于种子空间不足而再次遇到问题(再次,您正在寻找五百亿种独特的可能性。)另一方面,如果您要处理大量迭代,那么是的,您的低种子空间可能会破坏交易。
如果您要制作多人纸牌游戏,特别是如果有钱就行吗? 然后,您将需要对在线扑克站点如何处理您所询问的相同问题进行一些搜索。因为虽然低种子空间问题对于普通玩家而言并不明显,但值得花时间投资的情况却可以被利用。(所有扑克站点都经历了PRNG被“黑客入侵”的阶段,只需从裸露的扑克牌中推断出种子,就可以让某人看到所有其他玩家的底牌。)如果是这种情况,请不要“T能找到更好的PRNG -你需要尽可能认真地把它当作一个加密问题。
简短的解决方案,与dasblinkenlight基本上相同:
// Java 7
SecureRandom random = new SecureRandom();
// Java 8
SecureRandom random = SecureRandom.getInstanceStrong();
Collections.shuffle(deck, random);
您无需担心内部状态。详细解释原因:
当您以SecureRandom
这种方式创建实例时,它将访问特定于操作系统的真正随机数生成器。这是一个熵池,在其中访问的值包含随机位(例如,对于纳秒计时器,纳秒精度本质上是随机的),或者是内部硬件编号生成器。
此输入(!)可能仍包含虚假跟踪,被输入到加密强哈希中,该哈希将删除这些跟踪。这就是为什么使用这些CSPRNG而不是自己创建这些数字的原因!在SecureRandom
具有跟踪多少位被用于(一个计数器getBytes()
,getLong()
等)和笔芯的SecureRandom
熵比特必要时。
简而言之:只需忘记异议并SecureRandom
用作真正的随机数生成器。
如果您认为数字只是位(或字节)数组,那么您可以使用Random.nextBytes
此Stack Overflow问题中建议的(安全)解决方案,然后将该数组映射为new BigInteger(byte[])
。
一种非常简单的算法是将SHA-256应用于从0向上递增的整数序列。如果我们假设SHA-256的输出为“好,因为” 0之间均匀分布的整数,2(A盐可以根据需要,以“得到一个不同的序列”被追加。)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]
Random
永远不是真正的随机数。这是PRNG,其中P代表“伪”。对于真正的随机数,您需要一个随机性源(例如random.org)。