【问题标题】:Can Fisher-Yates shuffle produce all playing card permutations?Fisher-Yates shuffle 能否产生所有扑克牌排列?
【发布时间】:2019-12-11 11:01:26
【问题描述】:

我正在使用标准的 Fisher-Yates 算法来随机洗牌阵列中的一副牌。但是,我不确定这是否真的会产生真实世界洗牌后的一副牌的所有可能排列的真实分布。

V8 的 Math.random 只有 128 位的内部状态。由于一副牌中有 52 张牌,因此 52 个阶乘将需要 226 位的内部状态来生成所有可能的排列。

但是,我不确定这在使用 Fisher-Yates 时是否适用,因为您实际上并没有生成每个可能的位置,而只是从 52 个位置中随机获得一个位置。

function shuffle(array) {
  var m = array.length, t, i;

  while (m) {
    i = Math.floor(Math.random() * m--);
    t = array[m];
    array[m] = array[i];
    array[i] = t;
  }

  return array;
}

【问题讨论】:

  • 正确。每次调用 random 都会生成一张牌的位置,而不是整个牌组的状态。
  • 如果你没有足够的比特位用于完整状态,它不会随着时间的推移而产生偏差吗?
  • 是的。我错了,@Bergi 很好地解释了原因。
  • 我试图找出数字 226 的来源。如果我们对所有排列进行编号,这似乎与选择 1 到 52 之间的数字是同一个问题!而ceil(log(52!)/log(2)) 只有 220。另外 6 位是干什么用的?我们是否出于某种原因添加了ceil(log(52)/log(2)) 位?

标签: javascript arrays random shuffle


【解决方案1】:

一般来说,如果一个伪随机数生成器接受的阶乘不同种子少于 52 个,那么当它对 52 项列表进行洗牌时,会有一些排列特定的 PRNG 无法选择,而 Fisher-耶茨无法改变这一点。 (特定 PRNG 可以选择的排列集可能与另一个 PRNG 可以选择的排列集不同,即使两个 PRNG 都使用相同的种子进行初始化。)另见this question

请注意,尽管在撰写本文时,V8 使用的 Math.random 算法承认大约 2^128 个种子中的任何一个,但 Math.random 的 ECMAScript 规范并未强制要求特定的随机数算法,它仅指出方法使用“依赖于实现的算法或策略”来生成随机数(参见 ECMAScript sec. 20.2.2.27)。


可以通过 Bays-Durham shuffle 延长 PRNG 的周期,这有效地增加了 PRNG 的状态长度(请参阅 Severin Pappadeux 的回答)。但是,如果您仅使用 PRNG 的输出初始化 Bays-Durham 表条目(而不是使用种子来初始化这些条目),那个特定的 PRNG(包括它初始化这些条目并根据它生成的随机数选择那些表条目的方式)不能选择比可能的种子数更多的排列来初始化其原始状态,因为只有一种方法来初始化 Bays-给定种子的达勒姆条目——当然,除非 PRNG 实际上洗牌的列表数量过多,以至于它在没有循环的情况下生成的随机数比没有 Bays-Durham 洗牌时产生的随机数更多。

例如,如果 PRNG 的长度为 128 位,则只有 2^128 个可能的种子,因此只有 2^128 种方法来初始化 Bays-Durham shuffle,每个种子一个,除非超过 128 位的种子扩展到 Bays-Durham 表条目,而不仅仅是 PRNG 的原始状态。 (这并不意味着 PRNG 可以选择的排列集总是相同的,无论它如何选择 Bays-Durham shuffle 中的表条目。)

编辑(8 月 7 日):澄清。

编辑(2020 年 1 月 7 日):已编辑。

【讨论】:

  • 嗯,状态(和期间)可以轻松扩展
  • @Severin Pappadeux:已编辑。
  • 这真的很有帮助,尽管我仍然不确定。在 JavaScript 中不能做到这一点吗?
  • 您必须找到状态长度为 226 位或更多位的 PRNG 的 JavaScript 实现和supply a seed to that PRNGrecommend specific PRNGs 或描述如何为该 PRNG 生成合适的种子超出了此问题的范围。不过,一般而言,JavaScript 中的大多数 PRNG 实现并不适合信息安全,这与 window.cryptocrypto 中的方法不同,其中包括加密随机数生成器。
  • because there would be only one way to initialize the Bays-Durham entries for a given seed. 这当然是不正确的。 B-D shuflle 实现可以使用不同的索引策略来处理表。基本上,如果您有 128 个条目表,如果您从随机值的开头选择 7 位作为索引。或者你可以从值的末尾选择 7 位,或者你可以从中间选择,或者你可以恢复它等等。每种可能性都会产生不同的扩展序列。
【解决方案2】:

嗯,你肯定需要 226 位周期的 RNG 才能覆盖所有排列,@PeterO 的答案在这方面是正确的。但是您可以使用Bays-Durham shuffle 延长期限,通过有效地延长 RNG 的状态来支付费用。 B-D shuffled RNG的周期有一个估计,是

P = sqrt(Pi * N! / (2*O))

其中 Pi=3.1415...,N 是 B-D 表大小,O 是原始生成器的周期。如果取整个表达式的 log2,并使用斯特林公式进行阶乘,并假设 P=2226 和 O=2128,您可以在 BD 算法中获得 N 的估计值,即表的大小。从信封背面计算 N=64 就足以得到所有排列。

更新

好的,这是一个使用 B-D shuffle 扩展的 RNG 的示例实现。首先,我在 Javascript Xorshift128+ 中实现,使用 BigInt,这显然也是 V8 引擎中的默认 RNG。与 C++ 相比,它们在前几十个调用中产生了相同的输出。 128 位种子作为两个 64 位字。 Windows 10 x64,NodeJS 12.7。

const WIDTH = 2n ** 64n;
const MASK  = WIDTH - 1n; // to keep things as 64bit values

class XorShift128Plus { // as described in https://v8.dev/blog/math-random
    _state0 = 0n;
    _state1 = 0n;

    constructor(seed0, seed1) { // 128bit seed as 2 64bit values 
        this._state0 = BigInt(seed0) & MASK;
        this._state1 = BigInt(seed1) & MASK;
        if (this._state0 <= 0n)
            throw new Error('seed 0 non-positive');
        if (this._state1 <= 0n)
            throw new Error('seed 1 non-positive');
    }

    next() {
        let s1 = this._state0;
        let s0 = this._state1;
        this._state0 = s0;
        s1  = ((s1 << 23n) ^ s1 ) & MASK;
        s1 ^= (s1 >> 17n);
        s1 ^= s0;
        s1 ^= (s0 >> 26n);
        this._state1 = s1;
        return (this._state0 + this._state1) & MASK; // modulo WIDTH
    }
}

好的,然后在 XorShift128+ 之上,我实现了 BD shuffle,表大小为 4。为了您的目的,您需要表超过 84 个条目,并且两个表的幂更容易处理,所以让我们说128个条目表(7位索引)就足够了。无论如何,即使有 4 个条目表和 2 位索引,我们也需要知道要选择哪些位来形成索引。在原始论文中,B-D 讨论了从 rv 的后面以及从 rv 的前面等选择它们。这是 B-D shuffle 需要另一个种子值的地方 - 告诉算法从位置 2 和 6 中选择位。

class B_D_XSP {
    _xsprng;
    _seedBD = 0n;

    _pos0 = 0n;
    _pos1 = 0n;

    _t; // B-D table, 4 entries
    _Z = 0n;

    constructor(seed0, seed1, seed2) { // note third seed for the B-D shuffle
        this._xsprng = new XorShift128Plus(seed0, seed1);

        this._seedBD = BigInt(seed2) & MASK;
        if (this._seedBD <= 0n)
            throw new Error('B-D seed non-positive');

        this._pos0 = findPosition(this._seedBD);                         // first  non-zero bit position
        this._pos1 = findPosition(this._seedBD & (~(1n << this._pos0))); // second non-zero bit position

        // filling up table and B-D shuffler
        this._t = new Array(this._xsprng.next(), this._xsprng.next(), this._xsprng.next(), this._xsprng.next());
        this._Z = this._xsprng.next();
    }

    index(rv) { // bit at first position plus 2*bit at second position
        let idx = ((rv >> this._pos0) & 1n) + (((rv >> this._pos1) & 1n) << 1n);
        return idx;
    }

    next() {
        let retval = this._Z;
        let j = this.index(this._Z);
        this._Z = this._t[j];
        this._t[j] = this._xsprng.next();
        return retval;
    }
}

使用示例如下。

let rng = new B_D_XSP(1, 2, 4+64); // bits at second and sixth position to make index

console.log(rng._pos0.toString(10));
console.log(rng._pos1.toString(10));

console.log(rng.next());
console.log(rng.next());
console.log(rng.next());

显然,例如 8+128 的第三个种子值会产生与示例中显示的不同的排列,您可以使用它。

最后一步是通过调用几次(4 次中的 3 次)BD shuffled rng 生成 226 位随机值,并结合 64 位值(和潜在的结转)生成 226 个随机位,然后将它们转换为牌组 shuffle。

【讨论】:

  • 你能更详细地解释你的计算吗?还是你的公式有错误?在 n=64 和 O=2^128 的情况下,P 的 log2 甚至没有接近 256;它更像是 84.3。
  • @PeterO。好吧,阶乘中的主要术语是N log_2(N),所以从简单的表达式N log_2(N)=log_2(O)+log_2(P)=226+128=354你可以估计N。如果你把它精确到 84,那就这样吧。重点是,您可以通过 B-D shuffle 和支付较大的桌子来延长期限。
【解决方案3】:

你是对的。使用 128 位的起始状态,您最多只能生成 2128 个不同的排列。无论您多久使用一次此状态(致电Math.random()),PRNG 都是确定性的。

调用Math.random() 的次数实际上很重要

  • 每次调用都会将更多熵(例如来自硬件随机数)引入系统,而不是依赖于仅初始化一次的内部状态
  • 单个调用结果的熵非常低,以至于您不会在算法运行期间使用整个内部状态

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-03-06
    • 2011-03-21
    • 2013-10-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多