java.util.Random 真的那么随机吗?我怎样才能生成52! (阶乘)可能的序列?
Is java.util.Random really that random? How can I generate 52! (factorial) possible sequences?
我一直在使用 Random (java.util.Random)
洗一副 52 张牌。有52个! (8.0658175e+67) 种可能性。然而,我发现 java.util.Random
的种子是 long
,它在 2^64 (1.8446744e+19) 处要小得多。
从这里开始,我怀疑 java.util.Random
是否真的那么随机 ;它真的能够生成所有 52 个吗?可能性?
如果不是,我怎样才能可靠地生成一个更好的随机序列,可以生成所有 52 个!可能性?
一般来说,如果伪随机数生成器 (PRNG) 的最大循环长度小于 226 位,则无法从 52 项列表的所有排列中进行选择。
java.util.Random
实现了一个模数为 248 且最大循环长度仅为 48 位的算法,比我提到的 226 位少得多。您将需要使用另一个具有更大循环长度的 PRNG,特别是最大循环长度为 52 阶乘或更大的 PRNG。
另请参阅我的 article on random number generators 中的“洗牌”。
这种考虑与 PRNG 的性质无关;它同样适用于加密和非加密 PRNG(当然,只要涉及信息安全,非加密 PRNG 就不合适)。
尽管 java.security.SecureRandom
允许传入无限长度的种子,但 SecureRandom
实现可以使用底层 PRNG(例如,“SHA1PRNG”或“DRBG”)。这取决于 PRNG 的最大循环长度是否能够从 52 个阶乘排列中进行选择。
您的分析是正确的:使用任何特定种子播种伪随机数生成器在洗牌后必须产生相同的序列,将您可以获得的排列数限制为 264.这个断言是 easy to verify experimentally 通过调用 Collection.shuffle
两次,传递一个用相同种子初始化的 Random
对象,并观察到两次随机洗牌是相同的。
解决这个问题的方法是使用允许更大种子的随机数生成器。 Java 提供 SecureRandom
class 可以用几乎无限大小的 byte[]
数组初始化。然后,您可以将 SecureRandom
的实例传递给 Collections.shuffle
以完成任务:
byte seed[] = new byte[...];
Random rnd = new SecureRandom(seed);
Collections.shuffle(deck, rnd);
如果您将数字视为一个位(或字节)数组,那么也许您可以使用此 Stack Overflow 问题中建议的(安全)Random.nextBytes
解决方案,然后映射该数组进入 new BigInteger(byte[])
.
选择随机排列同时需要比您的问题所暗示的更多和更少的随机性。让我解释一下。
坏消息:需要更多的随机性。
您的方法的根本缺陷是它试图使用 64 位熵(随机种子)在 ~2226 种可能性之间进行选择。要在 ~2226 种可能性之间做出公平选择,您将不得不找到一种方法来生成 226 位熵而不是 64.
有几种生成随机位的方法:dedicated hardware, CPU instructions, OS interfaces, online services。在你的问题中已经有一个隐含的假设,你可以以某种方式生成 64 位,所以只要做你想做的事,只做四次,并将多余的位捐赠给慈善机构。 :)
好消息:需要更少的随机性。
一旦你有了这 226 个随机位,剩下的就可以确定地完成,因此 java.util.Random
的属性可以变得无关紧要。方法如下。
假设我们生成了全部 52 个!排列(请耐心等待)并按字典顺序对它们进行排序。
要选择一个排列,我们只需要一个介于 0
和 52!-1
之间的随机整数。该整数是我们的 226 位熵。我们将使用它作为我们排序的排列列表的索引。如果随机索引是均匀分布的,您不仅可以保证可以选择所有排列,而且它们将被选择 equiprobably(这比问题所要求的更有保证)。
现在,您实际上不需要生成所有这些排列。考虑到它在我们假设的排序列表中随机选择的位置,您可以直接生成一个。这可以使用 Lehmer[1] code (also see numbering permutations and factoriadic number system) 在 O(n2) 时间内完成。这里的 n 是你牌组的大小,即 52。
此 Whosebug answer 中有一个 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 混淆。 :)
让我提前道歉,因为这有点难以理解...
首先,您已经知道 java.util.Random
根本不是完全随机的。它以完全可预测的方式从种子生成序列。你是完全正确的,因为种子只有 64 位长,它只能生成 2^64 个不同的序列。如果您要以某种方式生成 64 个真正的随机位并将它们用于 select 个种子,您将无法使用该种子在 52 个中的 所有 之间随机选择!等概率的可能序列。
但是,这个事实无关紧要 只要您实际上不会生成超过 2^64 个序列,只要什么都没有 'special' 或 'noticeably special' 关于它 可以 生成的 2^64 序列。
假设您有一个使用 1000 位种子的更好的 PRNG。想象一下,您有两种初始化它的方法——一种方法是使用整个种子对其进行初始化,另一种方法是在初始化之前将种子散列到 64 位。
如果您不知道哪个初始化器是哪个,您可以编写任何类型的测试来区分它们吗?除非你(不幸)足够幸运最终用 same 64 位初始化坏的两次,否则答案是否定的。如果不详细了解特定 PRNG 实现中的某些弱点,您将无法区分这两个初始化程序。
或者,假设 Random
class 有一个 2^64 序列的数组,这些序列在遥远过去的某个时间完全随机地 selected,并且seed 只是这个数组的一个索引。
所以 Random
仅使用 64 位作为其种子这一事实实际上 不是 从统计学上来说一定是个问题,只要您不会使用相同的种子两次。
当然,对于 加密 目的,64 位种子是不够的,因为让系统使用相同的种子两次在计算上是可行的。
编辑:
我应该补充一点,尽管以上所有内容都是正确的,但 java.util.Random
的实际实现并不出色。如果您正在编写纸牌游戏,可以使用 MessageDigest
API 生成 "MyGameName"+System.currentTimeMillis()
的 SHA-256 哈希值,并使用这些位来洗牌。通过上面的论证,只要你的用户不是真的在赌博,你就不用担心currentTimeMillis
returns长了。如果您的用户 真的在赌博,那么使用 SecureRandom
没有种子。
我将对此采取一些不同的策略。您的假设是正确的 - 您的 PRNG 无法达到全部 52 个!可能性。
问题是:你的纸牌游戏规模是多少?
如果您正在制作一个简单的克朗代克风格的游戏?那么您肯定不需要全部 52 个!可能性。相反,可以这样看:一个玩家将有 18 quintillion 个不同的游戏。即使考虑到 'Birthday Problem',他们也必须玩数十亿手才能 运行 进入第一场重复游戏。
如果您要进行蒙特卡洛模拟?那么您可能没问题。由于 PRNG 中的 'P',您可能不得不处理工件,但您可能不会仅仅因为种子 space 低而 运行 陷入问题(同样,您看看 quintillions 的独特可能性。)另一方面,如果你正在处理大量迭代计数,那么,是的,你的低种子 space 可能会破坏交易。
如果您正在制作一款多人纸牌游戏,尤其是在有钱的情况下?那么您需要在谷歌上搜索在线扑克网站的方式处理了您所询问的相同问题。因为虽然低种子 space 问题对普通玩家来说 不明显 ,但如果值得投入时间,它是 可利用的 。 (扑克网站都经历过一个阶段,他们的 PRNG 是 'hacked',让某人看到所有其他玩家的底牌,只需从公开的牌中推导出种子。)如果这是你所处的情况, 不要 简单地找到一个更好的 PRNG - 你需要像对待加密问题一样认真对待它。
与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
作为真正的随机数生成器。
一个非常简单的算法是将 SHA-256 应用于从 0 向上递增的整数序列。 (如果需要,可以在 "get a different sequence" 上附加盐。)如果我们假设 SHA-256 的输出是 "as good as" 0 到 2256 之间的均匀分布整数- 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]
我的实证研究结果是 Java.Random 并不是完全随机的。如果您尝试使用随机 class“nextGaussian()”-方法并为 -1 和 1 之间的数字生成足够大的样本总体,则该图是正常的分布式场,称为高斯模型。
芬兰政府拥有的博彩书签器全年每天一次抽奖游戏,中奖 table 表明书签器以正常分配方式提供奖金。我的 Java 500 万次抽奖模拟显示,使用 nextInt() -methdod 抽奖时,奖金的正态分布与我的 Bookmarker 在每次抽奖中处理奖金的方式相同。
我的最佳选择是避免在每个结尾中出现数字 3 和 7,这是真的,它们很少出现在获胜结果中。有几次通过在 1-70(基诺)之间的整数中避免 3 和 7 个数字,赢得了 5 个选秀权中的 5 个。
芬兰彩票每周周六晚上开奖一次 如果您使用 39 个中的 12 个号码玩系统游戏,您可能会通过避免 3 和 7 个值而在优惠券中获得 5 或 6 个正确的选择。
芬兰彩票有 1-40 号可供选择,需要 4 张优惠券才能覆盖所有 12 号系统的 n 号。总成本为 240 欧元,从长远来看,对于经常赌徒来说,在不破产的情况下玩游戏太贵了。即使你分享优惠券给其他可以购买的客户,如果你想获利,你也必须非常幸运。
我一直在使用 Random (java.util.Random)
洗一副 52 张牌。有52个! (8.0658175e+67) 种可能性。然而,我发现 java.util.Random
的种子是 long
,它在 2^64 (1.8446744e+19) 处要小得多。
从这里开始,我怀疑 java.util.Random
是否真的那么随机 ;它真的能够生成所有 52 个吗?可能性?
如果不是,我怎样才能可靠地生成一个更好的随机序列,可以生成所有 52 个!可能性?
一般来说,如果伪随机数生成器 (PRNG) 的最大循环长度小于 226 位,则无法从 52 项列表的所有排列中进行选择。
java.util.Random
实现了一个模数为 248 且最大循环长度仅为 48 位的算法,比我提到的 226 位少得多。您将需要使用另一个具有更大循环长度的 PRNG,特别是最大循环长度为 52 阶乘或更大的 PRNG。
另请参阅我的 article on random number generators 中的“洗牌”。
这种考虑与 PRNG 的性质无关;它同样适用于加密和非加密 PRNG(当然,只要涉及信息安全,非加密 PRNG 就不合适)。
尽管 java.security.SecureRandom
允许传入无限长度的种子,但 SecureRandom
实现可以使用底层 PRNG(例如,“SHA1PRNG”或“DRBG”)。这取决于 PRNG 的最大循环长度是否能够从 52 个阶乘排列中进行选择。
您的分析是正确的:使用任何特定种子播种伪随机数生成器在洗牌后必须产生相同的序列,将您可以获得的排列数限制为 264.这个断言是 easy to verify experimentally 通过调用 Collection.shuffle
两次,传递一个用相同种子初始化的 Random
对象,并观察到两次随机洗牌是相同的。
解决这个问题的方法是使用允许更大种子的随机数生成器。 Java 提供 SecureRandom
class 可以用几乎无限大小的 byte[]
数组初始化。然后,您可以将 SecureRandom
的实例传递给 Collections.shuffle
以完成任务:
byte seed[] = new byte[...];
Random rnd = new SecureRandom(seed);
Collections.shuffle(deck, rnd);
如果您将数字视为一个位(或字节)数组,那么也许您可以使用此 Stack Overflow 问题中建议的(安全)Random.nextBytes
解决方案,然后映射该数组进入 new BigInteger(byte[])
.
选择随机排列同时需要比您的问题所暗示的更多和更少的随机性。让我解释一下。
坏消息:需要更多的随机性。
您的方法的根本缺陷是它试图使用 64 位熵(随机种子)在 ~2226 种可能性之间进行选择。要在 ~2226 种可能性之间做出公平选择,您将不得不找到一种方法来生成 226 位熵而不是 64.
有几种生成随机位的方法:dedicated hardware, CPU instructions, OS interfaces, online services。在你的问题中已经有一个隐含的假设,你可以以某种方式生成 64 位,所以只要做你想做的事,只做四次,并将多余的位捐赠给慈善机构。 :)
好消息:需要更少的随机性。
一旦你有了这 226 个随机位,剩下的就可以确定地完成,因此 java.util.Random
的属性可以变得无关紧要。方法如下。
假设我们生成了全部 52 个!排列(请耐心等待)并按字典顺序对它们进行排序。
要选择一个排列,我们只需要一个介于 0
和 52!-1
之间的随机整数。该整数是我们的 226 位熵。我们将使用它作为我们排序的排列列表的索引。如果随机索引是均匀分布的,您不仅可以保证可以选择所有排列,而且它们将被选择 equiprobably(这比问题所要求的更有保证)。
现在,您实际上不需要生成所有这些排列。考虑到它在我们假设的排序列表中随机选择的位置,您可以直接生成一个。这可以使用 Lehmer[1] code (also see numbering permutations and factoriadic number system) 在 O(n2) 时间内完成。这里的 n 是你牌组的大小,即 52。
此 Whosebug answer 中有一个 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 混淆。 :)
让我提前道歉,因为这有点难以理解...
首先,您已经知道 java.util.Random
根本不是完全随机的。它以完全可预测的方式从种子生成序列。你是完全正确的,因为种子只有 64 位长,它只能生成 2^64 个不同的序列。如果您要以某种方式生成 64 个真正的随机位并将它们用于 select 个种子,您将无法使用该种子在 52 个中的 所有 之间随机选择!等概率的可能序列。
但是,这个事实无关紧要 只要您实际上不会生成超过 2^64 个序列,只要什么都没有 'special' 或 'noticeably special' 关于它 可以 生成的 2^64 序列。
假设您有一个使用 1000 位种子的更好的 PRNG。想象一下,您有两种初始化它的方法——一种方法是使用整个种子对其进行初始化,另一种方法是在初始化之前将种子散列到 64 位。
如果您不知道哪个初始化器是哪个,您可以编写任何类型的测试来区分它们吗?除非你(不幸)足够幸运最终用 same 64 位初始化坏的两次,否则答案是否定的。如果不详细了解特定 PRNG 实现中的某些弱点,您将无法区分这两个初始化程序。
或者,假设 Random
class 有一个 2^64 序列的数组,这些序列在遥远过去的某个时间完全随机地 selected,并且seed 只是这个数组的一个索引。
所以 Random
仅使用 64 位作为其种子这一事实实际上 不是 从统计学上来说一定是个问题,只要您不会使用相同的种子两次。
当然,对于 加密 目的,64 位种子是不够的,因为让系统使用相同的种子两次在计算上是可行的。
编辑:
我应该补充一点,尽管以上所有内容都是正确的,但 java.util.Random
的实际实现并不出色。如果您正在编写纸牌游戏,可以使用 MessageDigest
API 生成 "MyGameName"+System.currentTimeMillis()
的 SHA-256 哈希值,并使用这些位来洗牌。通过上面的论证,只要你的用户不是真的在赌博,你就不用担心currentTimeMillis
returns长了。如果您的用户 真的在赌博,那么使用 SecureRandom
没有种子。
我将对此采取一些不同的策略。您的假设是正确的 - 您的 PRNG 无法达到全部 52 个!可能性。
问题是:你的纸牌游戏规模是多少?
如果您正在制作一个简单的克朗代克风格的游戏?那么您肯定不需要全部 52 个!可能性。相反,可以这样看:一个玩家将有 18 quintillion 个不同的游戏。即使考虑到 'Birthday Problem',他们也必须玩数十亿手才能 运行 进入第一场重复游戏。
如果您要进行蒙特卡洛模拟?那么您可能没问题。由于 PRNG 中的 'P',您可能不得不处理工件,但您可能不会仅仅因为种子 space 低而 运行 陷入问题(同样,您看看 quintillions 的独特可能性。)另一方面,如果你正在处理大量迭代计数,那么,是的,你的低种子 space 可能会破坏交易。
如果您正在制作一款多人纸牌游戏,尤其是在有钱的情况下?那么您需要在谷歌上搜索在线扑克网站的方式处理了您所询问的相同问题。因为虽然低种子 space 问题对普通玩家来说 不明显 ,但如果值得投入时间,它是 可利用的 。 (扑克网站都经历过一个阶段,他们的 PRNG 是 'hacked',让某人看到所有其他玩家的底牌,只需从公开的牌中推导出种子。)如果这是你所处的情况, 不要 简单地找到一个更好的 PRNG - 你需要像对待加密问题一样认真对待它。
与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
作为真正的随机数生成器。
一个非常简单的算法是将 SHA-256 应用于从 0 向上递增的整数序列。 (如果需要,可以在 "get a different sequence" 上附加盐。)如果我们假设 SHA-256 的输出是 "as good as" 0 到 2256 之间的均匀分布整数- 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]
我的实证研究结果是 Java.Random 并不是完全随机的。如果您尝试使用随机 class“nextGaussian()”-方法并为 -1 和 1 之间的数字生成足够大的样本总体,则该图是正常的分布式场,称为高斯模型。
芬兰政府拥有的博彩书签器全年每天一次抽奖游戏,中奖 table 表明书签器以正常分配方式提供奖金。我的 Java 500 万次抽奖模拟显示,使用 nextInt() -methdod 抽奖时,奖金的正态分布与我的 Bookmarker 在每次抽奖中处理奖金的方式相同。
我的最佳选择是避免在每个结尾中出现数字 3 和 7,这是真的,它们很少出现在获胜结果中。有几次通过在 1-70(基诺)之间的整数中避免 3 和 7 个数字,赢得了 5 个选秀权中的 5 个。
芬兰彩票每周周六晚上开奖一次 如果您使用 39 个中的 12 个号码玩系统游戏,您可能会通过避免 3 和 7 个值而在优惠券中获得 5 或 6 个正确的选择。
芬兰彩票有 1-40 号可供选择,需要 4 张优惠券才能覆盖所有 12 号系统的 n 号。总成本为 240 欧元,从长远来看,对于经常赌徒来说,在不破产的情况下玩游戏太贵了。即使你分享优惠券给其他可以购买的客户,如果你想获利,你也必须非常幸运。