如何在Java中获得足够的洗牌熵?

How to get sufficient entropy for shuffling cards in Java?

我正在从事涉及纸牌和洗牌的统计项目,我 运行 遇到了随机数生成问题。

根据简单的数学运算,有 52 个!可能的牌组排列,大约为 2^226。我相信这意味着我需要一个至少有 226 位熵的随机数生成器,可能更多(我不确定这个概念,所以任何帮助都会很好)。

通过快速 google 搜索,Java 中的 Math.random() 生成器具有最多 48 位的熵,这意味着绝大部分可能的套牌组合不会被表示.所以这似乎不是进入 Java.

的方式

我已链接到 this 生成器,但它还没有 java 实现。此外,这里还有一些上下文是我的洗牌算法之一(它使用 Fisher-Yates 方法)。如果您对提高代码效率有任何建议,那也太棒了。

public void shuffle(int type, int swaps){
    int[] newDeck = getNewDeck();

    if(type == 1){
        for(int i = 0; i < 52; i++){
            int nextCardIndex = (int)(Math.random()*newDeck.length);
            deck[i] = newDeck[nextCardIndex];
            newDeck = removeItem(nextCardIndex, newDeck);
        }
    }
}

public int[] getNewDeck(){
    int[] newDeck = new int[52];
    for(int i = 1; i <= 52; i++){
        newDeck[i-1] = i;
    }
    return newDeck;
}

public int[] removeItem(int index, int[] array){
    int[] newArray = new int[array.length-1];
    for(int i = 0; i < index; i++){
        newArray[i] = array[i];
    }
    for(int i = index; i < array.length-1; i++){
        newArray[i] = array[i+1];
    }

    array = newArray;
    return array;
}

您是否查看过 JDK 17 中最近添加的内容?

https://docs.oracle.com/en/java/javase/17/core/pseudorandom-number-generators.html#GUID-08E418B9-036F-4D11-8E1C-5EB19B23D8A1

有很多可用的算法:

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/package-summary.html#algorithms

对于洗牌,您可能不需要加密安全的东西。

如果您提供了不错的 RNG,使用 Collections.shuffle 应该可以解决问题。

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collections.html#shuffle(java.util.List,java.util.Random)

简答:

  • 你混淆了均匀性和熵。
  • Java 的内置代码在数学上已经很完美了。您无法使用不同的 pseudo-random 数字生成器来改进这些。
  • Collections.shuffle(deck) 就是你所需要的。真的。这是完美的。统一,不可预测table.

但是,不要相信我的话。我会证明的。

均匀性和熵 - 根本不是一回事。

I'm working on a statistics project involving cards and shuffling, and I've run across an issue with random number generation. From a simple bit of math there are 52! possible deck permutations, which is approximately 2^226. I believe this means that I need a random number generator with a minimum of 226 bits of entropy,

你混淆了概念。有均匀分布,可以在熵为 0 的情况下很好地完成,还有不可预测性,这需要熵。 “226 位熵”与您有大约 2^226 种不同排列可用的想法完全无关。算法的输出序列不会或多或少地成为 predictable 基于算法的任何给定 'output' 可以提供的选择数量。

好的,什么是排列均匀性?

让我们坚持掷骰子,这比洗牌更可行。我们有标准的 6 面骰子,我们编写了一个算法来掷骰子。我们的第一次尝试是这样的:

将 运行dom 算法视为黑盒。假设您不知道实现是什么,您只知道:我可以调用它,一个 运行dom 号码掉了。

如果你调用一次,你不知道它坏了。它真的不是——它给出了一个数字,不知道那个代码就无法知道。它是完美的 运行dom 和完美的 unpredictable.

但是,多次调用它,现在它变得有趣了。很快你就会开始意识到一个数字的生成比另一个数字多得多。如果您 运行 这个算法 1000 次并制作直方图,您会注意到您有 1000 个 '4' 和零个 1/2/3/5/6。这说明该算法显得高度non-uniform,它提供每个数字的频率并不相等。那或者你非常倒霉,这总是可能的(如果你真的掷骰子一千次,十亿十亿年一次,它会连续一千次每次都掷出 4,纯属偶然。任何算法理论上无法做到这一点 less 运行dom 比真正的 运行dom 源!)

但这是另一种算法:

private int roll = 1;
int roll() {
  return roll = (roll + 1) % 6;
}

多次调用这个,直方图完美。每个数字出现的频率相同。它完全是 uniform - 没有一个数字比任何其他数字更有可能。如果我和你玩一个游戏,你预测我会掷什么,如果你预测正确,我会给你钱,但你不知道我之前调用那个方法的频率,你只能玩一次,你没有比 运行dom 机会更好的了。

与我们之前的 return 4; 对比,采取,您可以预测“4”的地方,您将获得现金。

这里显然是 0 熵,但它是完全均匀的。因此,为了获得一致性(使连续调用 'shuffle this deck!' 方法产生所有可能的洗牌方法的可能性相同,并且没有任何特定的顺序,如果只有你 运行 足够长了)- 我们以 0 熵 到达那里。这显然是极端预测table,但那是另一个问题。

让我们开始有趣的:

int roll() {
  return 1 + (int) (Math.random() * 6);
}

这看起来不错。 但不是,我可以证明。从数学上讲,它不可能是完全统一的。

Math.random() 产生 'any number between 0 and 1',并且,暂时忘记计算机,运行ge 中有无穷多个数字。但是,我们注意到 Math.random() 方法 return 是 double,并且 java 规范说它们占用 64 位。显然,您不能在 64 位中存储无限 space 中的选择;你需要无限位。此外,double 可以表示 Math.random() 不能 return 的数字(例如 2.0;有一个位序列是有效的 double,其值为 2.0,而 Math.random() 永远不会 return 它,因为它不在 0 和 1 之间),所以这就是你如何获得大约 48 位的 'variance'。不是'about 48 bits',正好是48位(但即使不是,下面的证明也成立):

48 位是 2^48。

这意味着我们的算法只有 2^48 个不同的潜在输出!我们可以制作一个非常、非常、非常庞大的 table,明确列出所有 2^48 个不同的输入,并写出 运行domly rolled 数字是什么。这个 table 里面会有 正好 2^48 个条目。这意味着 正好 2^48 乘以 1、2、3、4、5 或 6。

问题来了:2^48/6 不是整数。显然 - 2^48 只能被 2 整除(实际上是 48 次),而 6 是质数 2*3 - 显然它不会被干净地除尽。 table 中有 281474976710656 个条目,这意味着s, best case scenario, 6个数字中有4个出现46912496118443次,其中2个出现46912496118442次

这是一个非常非常小的差异,但是以上是证明 Math.random() 不可能完全统一。不像我们的方法,只是 returns 1-2-3-4-5-6 在一个序列中。

显然,如果你关心适当的 运行domness,你 do 想要那种统一性。在这里为 Math.random() 提供动力的熵的多少并不重要。这根本不是问题。

那么解决方法是什么?使用右边的API。如果您使用正确的方法,java.util.Random 完美的 统一。因此,我们的第四个也是最后一个可用的方法是:

Random r = new java.util.Random();

public int roll() {
  return 1 + r.nextInt(6);
}

nextInt 的 API 文档为您详细说明:它保证 运行 完全一致。

那什么是可预测性呢?

我们的方法return循环中的 1-2-3-4-5-6 是完美的预测table。让我们假设我们再次玩我们的游戏:你预测一个数字,我掷一次,如果是那个数字你会得到很多钱。但现在让我们添加一个皱纹:在你告诉我你想要的数字之前,你可以让我掷骰子并告诉你结果。你可以问我很多次这个问题,你可以把它们都写下来并考虑一下,你可以要求我除了你之外永远不要掷骰子(所以,当你问,或者当我们做钱卷)。

对于那个 1-2-3-4-5-6 算法,你每次都会赢。你只要让我滚动几次,你就会意识到它是 1-2-3-4-5-6-1-2-3-4 无休止的,并且预测完美。所以,这个是完全统一的,但也很容易仅根据观察数据进行预测。

这就是你需要熵的地方。熵是关于引入您知道的因素。这些未知因素就是熵。熵越大,您需要看到的数字就越多。例如,如果我被允许掷 5 个硬币并且我不需要告诉你结果,我可以将这些掷硬币的结果应用到我的掷骰子上,我可以说我将在第一个数字上加 1我告诉你 if 第一枚硬币是正面。我在第二个数字上加 1 我告诉你 如果 第二个硬币是正面,依此类推。在第 6 卷我 运行 出局,我需要 re-use 第一枚硬币。我是电脑,没有手; a CPU里面没有硬币,我不能再掷硬币了。

在这种情况下,你需要问我几次结果才能预测——你需要先算出抛硬币的次数。但是,你会的,一旦你问我要 5 卷。然后你又回到了完美的预测。

那是熵。

诀窍是,计算机一直在产生熵。想象一下这个替代算法:它仍然是最简单的:

int roll = 1;
public int roll() {
  return roll = (roll + 1) % 6;
}

但这一次,一千名玩家都在和我玩这个游戏,他们是人类(所以他们说:“请为我滚!”的速度不是很统一或预测table,人类是非常复杂的机器,数十亿个细胞都在相互作用,太难分析了)——所以现在实际上,即使我的算法看起来完全愚蠢,一个简单的事实是你没有得到连续的滚动,而是以一个未知的和unpredictable 在你之前问的人的数量,我们回到了完美:你在这里的 6 场比赛中不能超过 1 场。

这正是您的 OS 的工作方式。它使用 'hey, somebody wants a random number' 本身作为熵源。事实上,它还会向系统中注入任何难以预测的东西。想象一下,你 知道我掷骰子是为了踢球(例如,我在 1-2-3-4-5-6-1-2- 的序列中向前移动一个。 ..) 准确地每秒。您可以考虑到这一点,所以这是无用的,但它不会减少 熵。你只能添加,假设你正确地编写了你的​​算法。 OS 知道这一点,并尽可能多地添加。它将添加键盘输入和鼠标移动。即使键盘实际上是一个每秒按 space 的机器人,在点上,准确地——好吧,好吧,那里没有熵。这并没有使情况变得更糟。 (这是一个真正的“嘿,如果它没有帮助至少它不会伤害”的场景)。

因为这个:

  • 计算机不断接收大量的熵!网络数据包进入网卡、键盘和鼠标输入的时间,以及各种应用程序固有的熵 运行ning 在系统上从 OS 请求 运行dom 号码(以及 java 的 运行dom 内容,例如 Math.random()j.u.Random 只需询问 OS)。即使计算机以某种方式 'ran out' 熵,在几毫秒内它也会收集更多。
  • 随机性就像一个ever-filling池。如果您让计算机 运行 几个小时,稍微移动一下鼠标,那么您一直在累积熵。它堆积起来。在你遇到麻烦之前,你必须 'drain' 消除这个熵。
  • PRNG 在这里没有解决任何问题他们不能。如果你的计算机真的 'ran out' 熵(这是极不可能的,因为它有很多来源),没有 PRNG 可以拯救你。说到底,计算机就是没有硬币注1,不能无中生有熵。您可以想象的任何算法都无法改变这一点。充其量他们可以检测到我们 运行 没有熵,并且只是停止给你 运行dom 数字,直到它找到一些熵note 2.

是的,您至少需要 226 位的熵;如果你有更少,那么你的 'shuffle this deck' 代码可以证明 predictable 在某种意义上你将能够消除至少一些可能的甲板订单。但是你的系统有很多很多数百万位的熵可供你使用,你不会 运行 出来。但如果你这样做,PRNG 并不能神奇地解决这个问题。

正确的做法

因此,我们直截了当:如果我们玩我们的骰子预测游戏,我的目标是尽可能完美地设置它,即:无论你多么聪明,你都不可以想象在统计上比每 6 场比赛赢一场更好。

为了完成这个,我需要均匀性不可预测性:

  • 有了 1-2-3-4-5-6 的东西,您就可以预测并以这种方式获胜。它是统一的,但是,predictable.
  • 使用一种算法,比如说,在所有可以想象的方面都是真正完全不可预测的table,完美的熵,但是,它只是 return 1 或 2 的两倍。因为它是世界上最完美的骰子和世界上最大的 运行dom 骰子滚轮,但它滚动的是 8 面骰子,并且 returns 那 % 6(等等7 表示“1”,8 表示“2”)。在这种情况下,你可以每 4 场比赛赢一次,这比每 6 场比赛赢一次要好(每次预测 1;如果我在完美的 8 面骰子上掷出 1 或 7,你就赢了,平均每 4 卷发生一次)。

那么在 java 中如何做到这一点?琐碎的:

Collections.shuffle(deck);

就是这样。那个可笑的小班轮做到了这一切。

  • 非常均匀。正如文档所说,shuffle 使用 Fisher-Yates,Fisher-Yates 通过了鸽巢原理测试(它获得的 运行dom 源的排列数可以被 X 整除!其中X 是列表的大小。F-Y 上的 wiki 条目有完整的证明)。
  • 它使用java.util.Random,一般来说没问题。

还有 SecureRandom 试图做一些事情来降低可预测性。出于加密目的(例如生成加密密钥),您应该使用它。对于一个 运行 是扑克游戏的服务器,即使是花大价钱, java.util.Random 也可以。如果必须,请使用 SecureRandom,但我认为除了比其他窃取现金的方法复杂得多的方案之外,没有其他方法可以 'break'。


脚注 1

您可以获得可以掷硬币的定制硬件。通常这些是接收宇宙背景辐射的收音机。计算机还可以通过巧妙的方式 'detect' 在其自身的标准硬件上产生宇宙背景效果。我不确定芯片是否附带它,我想有些确实如此,并且您的 OS 将使用芯片提供的熵初始化其 运行dom 'pool'。当然,一台没有人机交互或来自互联网流量等不可预测table源的交互的计算机,这是一个非常隔离的房间,在法拉第笼之类的地方,这里根本没有熵,你可以无中生有。

脚注 2

在 linux /dev/urandom 给你无尽的 运行 domness,但 /dev/random 将开始 'blocking' (等待,像一个慢文件,直到熵显示向上)。因此,一些被误导的文档建议您使用 /dev/random 进行加密。但这是个糟糕的建议:计算机实际上不可能知道一些明显的熵块是否真的是熵。如果键盘是虚拟键盘,某个机器人每秒按 space,并且每个人都知道,那么这不会增加熵。但是计算机不知道这一点。它可以尝试 'analyse' 输入并意识到它似乎有一个模式,但有时如果你掷硬币十次,每次都是正面。这并没有减少 运行dom。如果你消除这样的模式,你实际上 less 运行dom.

因此,只需使用/dev/urandom。因此,java.util.Random 通常没问题。

一个旁注

虚拟 PC(例如 docker、kubernetes 等)确实可以 运行 摆脱熵。它的模拟芯片并不总是正确 'written' 并且在芯片的熵初始化中烘焙只是 returns 零,网络是 predictable,它都是脚本和防火墙,所以实际上是零熵流入系统。

这是早期采用虚拟 PC 映像概念的真正问题。但是现在,从 th 到 'pass entropy' 的所有各种实现主机 OS 到虚拟 PC,例如管理 /dev/urandom/dev/random 的 linux 驱动程序将其考虑在内。

说真的,别担心。使用 Collections.shuffle,例如rnd.nextInt()。不要使用 Math.random() 做任何事情。除非你真的真的需要一个介于 0 和 1 之间的数字。