java.util.Random真的那么随机吗?我该如何生成52!(部分)可能的顺序?


202

我一直在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个更好的随机序列!可能性?


21
“我如何确定生成超过52 的真实随机数!” 来自的数字Random永远不是真正的随机数。这是PRNG,其中P代表“伪”。对于真正的随机数,您需要一个随机性源(例如random.org)。
TJ Crowder

7
@JimGarrison这不是OP的要求。他正在谈论10 ^ 68个可能的序列。由于每个伪随机序列均由其种子识别,OP说最多可以有2 ^ 64个不同的序列。
dasblinkenlight

6
我认为这是一个有趣的问题,值得考虑。但我不禁要问您的问题背景:究竟是什么导致要求能够生成全部52个!排列?例如,在现实世界中的桥梁中,我们可以洗牌并一次发一张牌,但是只有大约6e11个不同的手牌,因为许多不同的排列导致同一手牌。在另一个方向上思考,您是否需要专门针对52!的解决方案,或者您需要一个泛化为两个混在一起的套牌(104!/(2 ** 52)可能性或〜2e150)的解决方案?
NPE

9
@NPE-以接龙(克朗代克)为例52!正是可能的手数..
Serj Ardovic

3
我认为这是一个有趣的读物:superuser.com/a/712583
Dennis_E

Answers:


153

选择一个随机排列同时需要比您的问题所暗示的更多或更少的随机性。让我解释。

坏消息:需要更多的随机性。

您的方法的根本缺陷在于,它试图使用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混淆。:)


7
嘿,我确定最后的链接是New Math。:-)
TJ Crowder

5
@TJCrowder:差不多了!摆动的是无限可微的黎曼流形。:-)
NPE

2
很高兴看到人们欣赏经典。:-)
TJ Crowder

3
您从Java的哪儿随机获得226位?抱歉,您的代码无法回答。
Thorsten S.

5
我不明白您的意思,Java Random()也不会提供64位的熵。OP表示一个未指定的源,可以产生64位以种子PRNG。假设可以向同一源请求226位是有意义的。
停止伤害莫妮卡

60

您的分析是正确的:使用任意特定种子播种伪随机数生成器后,必须在改组后产生相同的序列,从而将可以获取的排列数限制为2 64。通过两次调用,传递使用相同种子初始化的对象以及观察两个随机混洗是相同的,可以很容易地通过实验验证此断言。Collection.shuffleRandom

因此,解决方案是使用允许更大种子的随机数生成器。Java提供SecureRandom了可以用byte[]几乎无限大小的数组初始化的类。然后,您可以通过一个实例SecureRandomCollections.shuffle完成任务:

byte seed[] = new byte[...];
Random rnd = new SecureRandom(seed);
Collections.shuffle(deck, rnd);

8
当然,一个大种子并不能保证全部52个种子!可能会产生(此问题专门针对的是什么)?作为思想实验,请考虑一个病理PRNG,该PRNG可以获取任意大的种子并生成无限长的零序列。显然,PRNG不仅需要获取足够多的种子,还需要满足更多的要求。
NPE

2
@SerjArdovic是的,根据Java文档,传递给SecureRandom对象的任何种子材料都必须不可预测。
dasblinkenlight

10
@NPE没错,尽管种子数目太小是上限的保证,但种子数目太大并不能保证下限。所有这一切都消除了理论上限,RNG可以生成全部52个!组合。
dasblinkenlight

5
@SerjArdovic所需的最小字节数为29(您需要226位来表示52!可能的位组合,即28.25字节,因此我们必须将其四舍五入)。请注意,使用29字节的种子材料会删除理论上可以获得的随机播放次数的上限,而无需确定下限(请参阅NPE关于a脚的RNG的注释,该片段需要很大的种子并生成全零序列)。
dasblinkenlight

8
SecureRandom实现几乎肯定会使用基础PRNG。而且它是否可以从52个阶乘置换中进行选择取决于PRNG的周期(在较小程度上是状态长度)。(请注意,文档说该SecureRandom实现“至少符合”某些统计测试,并且生成的“必须具有强大的加密能力”,但对底层PRNG的状态长度或周期没有明确的下限。)
Peter O.

26

通常,如果伪随机数生成器(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在不缩短或压缩该种子的情况下可以用来初始化其状态的种子的最大大小”)。


18

我先向您道歉,因为这有点难以理解...

首先,您已经知道这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并不出色。如果您正在写纸牌游戏,则可以使用MessageDigestAPI生成的SHA-256哈希值"MyGameName"+System.currentTimeMillis(),然后使用这些位对卡片组进行洗牌。通过上面的论点,只要您的用户不是真的在赌博,您就不必担心currentTimeMillis返回的时间很长。如果你的用户真正的赌博,然后用SecureRandom无种子。


6
@ThorstenS,您怎么能编写任何测试来确定有些卡组合永远无法出现?
Matt Timmermans

2
有几种随机数测试套件,例如George Marsaglia的Diehard或Pierre L'Ecuyer / Richard Simard的TestU01,可以轻松地在随机输出中发现统计异常。对于卡检查,您可以使用两个正方形。您确定卡的顺序。第一个方框显示前两张卡的位置为xy对:第一张卡的位置为x,第二张卡的差异位置(-26-25)为y。第二个方块显示的是第三张和第四张卡片,相对于第二张/第三张,它们的(-25-25)如果运行一段时间,它将立即显示分布中的差距和集群
Thorsten S.

4
好吧,这不是您说过可以编写的测试,但是它也不适用。您为什么要假设分布中存在这样的测试会发现的差距和集群?正如我所提到的,这将意味着“ PRNG实施中的特定弱点”,并且与可能的种子数量完全无关。这样的测试甚至不需要您重新设置生成器的种子。一开始我确实警告过这很难理解。
马特·蒂默曼斯

3
@ThorstenS。这些测试套件绝对不会确定您的源是64位播种的加密安全PRNG还是真正的RNG。(毕竟,测试PRNG是这些套件的目的。)即使您知道所使用的算法,良好的PRNG也不可能在没有对状态空间进行强力搜索的情况下确定状态。
Sneftel

1
@ThorstenS .:在真正的纸牌中,绝不会出现绝大多数组合。您只是不知道那些是什么。对于一个像样的PRNG,它是相同的-如果您可以测试给定的输出序列在其图像中是否存在那么长,这就是PRNG的缺陷。荒谬的巨大状态/时期,例如52!不需要; 128位就足够了。
R .. GitHub停止帮助ICE,

10

我将对此采取不同的策略。您的假设是正确的-PRNG将无法达到全部52个目标!可能性。

问题是:您的纸牌游戏规模是多少?

如果您要制作一个简单的克朗代克风格的游戏? 那么您绝对不需要全部52!可能性。而是这样看:一个玩家将拥有18 亿个独特游戏。即使考虑到“生日问题”,他们也必须先打数十亿手才能进入第一个重复游戏。

如果要进行蒙特卡罗模拟? 那你可能还好。由于PRNG中的“ P”,您可能不得不处理工件,但可能不会仅由于种子空间不足而再次遇到问题(再次,您正在寻找五百亿种独特的可能性。)另一方面,如果您要处理大量迭代,那么是的,您的低种子空间可能会破坏交易。

如果您要制作多人纸牌游戏,特别是如果有钱就行吗? 然后,您将需要对在线扑克站点如何处理您所询问的相同问题进行一些搜索。因为虽然低种子空间问题对于普通玩家而言并不明显,但值得花时间投资的情况却可以被利用。(所有扑克站点都经历了PRNG被“黑客入侵”的阶段,只需从裸露的扑克牌中推断出种子,就可以让某人看到所有其他玩家的底牌。)如果是这种情况,请不要“T能找到更好的PRNG -你需要尽可能认真地把它当作一个加密问题。


9

简短的解决方案,与dasblinkenlight基本上相同:

// Java 7
SecureRandom random = new SecureRandom();
// Java 8
SecureRandom random = SecureRandom.getInstanceStrong();

Collections.shuffle(deck, random);

您无需担心内部状态。详细解释原因:

当您以SecureRandom这种方式创建实例时,它将访问特定于操作系统的真正随机数生成器。这是一个熵池,在其中访问的值包含随机位(例如,对于纳秒计时器,纳秒精度本质上是随机的),或者是内部硬件编号生成器。

此输入(!)可能仍包含虚假跟踪,被输入到加密强哈希中,该哈希将删除这些跟踪。这就是为什么使用这些CSPRNG而不是自己创建这些数字的原因!在SecureRandom具有跟踪多少位被用于(一个计数器getBytes()getLong()等)和笔芯的SecureRandom熵比特必要时

简而言之:只需忘记异议并SecureRandom用作真正的随机数生成器。


4

如果您认为数字只是位(或字节)数组,那么您可以使用Random.nextBytesStack Overflow问题中建议的(安全)解决方案,然后将该数组映射为new BigInteger(byte[])


3

一种非常简单的算法是将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]
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.