简体   繁体   English

Fisher-Yates shuffle 能否产生所有扑克牌排列?

[英]Can Fisher-Yates shuffle produce all playing card permutations?

I'm using the standard Fisher-Yates algorithm to randomly shuffle a deck of cards in an array.我正在使用标准的 Fisher-Yates 算法随机洗牌阵列中的一副牌。 However, I'm unsure if this will actually produce a true distribution of all possible permutations of a real-world shuffled deck of cards.但是,我不确定这是否真的会产生真实世界洗牌的一副扑克牌的所有可能排列的真实分布。

V8's Math.random only has 128-bits of internal state. V8 的Math.random只有 128 位的内部状态。 Since there are 52 cards in a deck, 52 factorial would require 226-bits of internal state to generate all possible permutations.由于一副牌中有 52 张牌,52 阶乘将需要 226 位的内部状态来生成所有可能的排列。

However, I'm unsure if this applies when using Fisher-Yates since you aren't actually generating each possible but just getting one position randomly out of 52.但是,我不确定这在使用 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;
}

In general, if a pseudorandom number generator admits fewer than 52 factorial different seeds, then there are some permutations that particular PRNG can't choose when it shuffles a 52-item list, and Fisher-Yates can't change that.一般来说,如果伪随机数生成器允许的不同种子少于 52 个,那么特定 PRNG在对 52 项列表进行洗牌时无法选择某些排列,而 Fisher-Yates 无法改变这一点。 (The set of permutations a particular PRNG can choose can be different from the set of permutations another PRNG can choose, even if both PRNGs are initialized with the same seed.) See also this question . (特定 PRNG 可以选择的排列集可能与另一个 PRNG 可以选择的排列集不同,即使两个 PRNG 都使用相同的种子初始化。)另请参阅此问题

Note that although the Math.random algorithm used by V8 admits any of about 2^128 seeds at the time of this writing, no particular random number algorithm is mandated by the ECMAScript specification of Math.random , which states only that that method uses an "implementation-dependent algorithm or strategy" to generate random numbers (see ECMAScript sec. 20.2.2.27).注意,虽然所述Math.random由V8使用的算法承认任何大约2 ^ 128种子在撰写本文时,还没有特定的随机数的算法是由ECMAScript规范规定Math.random ,其中指出只有该方法使用生成随机数的“依赖于实现的算法或策略”(参见 ECMAScript 第 20.2.2.27 节)。


A PRNG's period can be extended with the Bays-Durham shuffle, which effectively increases that PRNG's state length (see Severin Pappadeux's answer). PRNG 的周期可以通过 Bays-Durham shuffle 延长,这有效地增加了 PRNG 的状态长度(参见 Severin Pappadeux 的回答)。 However, if you merely initialize the Bays-Durham table entries with outputs of the PRNG (rather than use the seed to initialize those entries), it will still be the case that that particular PRNG (which includes the way in which it initializes those entries and selects those table entries based on the random numbers it generates) can't choose more permutations than the number of possible seeds to initialize its original state, because there would be only one way to initialize the Bays-Durham entries for a given seed — unless, of course, the PRNG actually shuffles an exorbitant amount of lists, so many that it generates more random numbers without cycling than it otherwise would without the Bays-Durham shuffle.但是,如果您仅使用 PRNG 的输出初始化 Bays-Durham 表条目(而不是使用种子来初始化这些条目),那么该特定 PRNG (包括它初始化这些条目的方式)仍然是这种情况并根据它生成的随机数选择那些表条目)不能选择比可能的种子数量更多的排列来初始化其原始状态,因为只有一种方法可以初始化给定种子的 Bays-Durham 条目——当然,除非 PRNG 实际上混洗了过多的列表,以至于它在不循环的情况下生成的随机数比没有 Bays-Durham 混洗时要多。

For example, if the PRNG is 128 bits long, there are only 2^128 possible seeds, so there are only 2^128 ways to initialize the Bays-Durham shuffle, one for each seed, unless a seed longer than 128 bits extends to the Bays-Durham table entries and not just the PRNG's original state.例如,如果 PRNG 长度为 128 位,则只有 2^128 个可能的种子,因此初始化Bays-Durham shuffle 的方法只有 2^128 种,每个种子一个,除非超过 128 位的种子扩展为Bays-Durham 表条目,而不仅仅是 PRNG 的原始状态。 (This is not to imply that the set of permutations that PRNG can choose is always the same no matter how it selects table entries in the Bays-Durham shuffle.) (这并不意味着 PRNG 可以选择的排列集始终相同,无论它如何选择 Bays-Durham shuffle 中的表条目。)

EDIT (Aug. 7): Clarifications.编辑(8 月 7 日):澄清。

EDIT (Jan. 7, 2020): Edited.编辑(2020 年 1 月 7 日):已编辑。

You are right.你是对的。 With 128 bits of starting state, you can only generate at most 2 128 different permutations.使用 128 位的起始状态,您最多只能生成 2 128种不同的排列。 It doesn't matter how often you use this state (call Math.random() ), the PRNG is deterministic after all.不管您使用这种状态的频率(调用Math.random() ),PRNG 毕竟是确定性的。

Where the number of calls to Math.random() actually matter is whenMath.random()的调用次数真正重要的地方是什么时候

  • each call would draw some more entropy (eg from hardware random) into the system, instead of relying on the internal state that is initialised only once每次调用都会将更多的熵(例如来自硬件随机)引入系统,而不是依赖于仅初始化一次的内部状态
  • the entropy of a single call result is so low that you don't use the entire internal state over the run of the algorithm单个调用结果的熵非常低,以至于您不会在算法运行期间使用整个内部状态

Well, you definitely need RNG with 226bits period for all permutation to be covered, @PeterO answer is correct in this regard.好吧,您肯定需要 226 位周期的 RNG 才能涵盖所有排列,@PeterO 在这方面的回答是正确的。 But you could extend period using Bays-Durham shuffle , paying by effectively extending state of RNG.但是你可以使用Bays-Durham shuffle来延长时间,通过有效地延长 RNG 的状态来支付。 There is an estimate of the period of the BD shuffled RNG and it is BD shuffle RNG 的周期有一个估计,它是

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

where Pi=3.1415..., N is BD table size, O is period of the original generator.其中 Pi=3.1415..., N是 BD 表大小, O是原始生成器的周期。 If you take log 2 of the whole expression, and use Stirling formula for factorial, and assume P=2 226 and O=2 128 , you could get estimate for N, size of the table in BD algorithm.如果您取整个表达式的 log 2 ,并使用斯特林公式进行阶乘,并假设 P=2 226和 O=2 128 ,您可以估计 N,BD 算法中表的大小。 From back-of-the envelope calculation N=64 would be enough to get all your permutations.从包络背面计算 N=64 就足以获得所有排列。

UPDATE更新

Ok, here is an example implementation of RNG extended with BD shuffle.好的,这里是使用 BD shuffle 扩展的 RNG 的示例实现。 First, I implemented in Javascript Xorshift128+, using BigInt, which is apparently default RNG in V8 engine as well.首先,我在 Javascript Xorshift128+ 中使用 BigInt 实现,这显然也是 V8 引擎中的默认 RNG。 Compared with C++ one, they produced identical output for first couple of dozen calls.与 C++ 相比,它们在前几十个调用中产生了相同的输出。 128bits seed as two 64bits words. 128 位种子作为两个 64 位字。 Windows 10 x64, NodeJS 12.7. 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
    }
}

Ok, then on top of XorShift128+ I've implemented BD shuffle, with table of size 4. For your purpose you'll need table more than 84 entries, and power of two table is much easier to deal with, so let's say 128 entries table (7bit index) shall be good enough.好的,然后在 XorShift128+ 之上,我已经实现了 BD shuffle,表的大小为 4。为了您的目的,您需要表超过 84 个条目,并且两个表的幂更容易处理,所以假设有 128 个条目表(7 位索引)应该足够好。 Anyway, even with 4 entries table and 2bit index we need to know which bits to pick to form index.无论如何,即使有 4 个条目表和 2 位索引,我们也需要知道选择哪些位来形成索引。 In original paper BD discussed picking them from the back of rv as well as from front of rv etc. Here is where BD shuffle needs another seed value - telling algorithm to pick say, bits from position 2 and 6.在原始论文中,BD 讨论了从 rv 的后面以及从 rv 的前面等选择它们。这里是 BD 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;
    }
}

Use example is as follow.使用示例如下。

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());

Obviously, third seed value of say 8+128 would produce different permutation from what is shown in the example, you could play with it.显然,假设 8+128 的第三个种子值会产生与示例中显示的不同的排列,您可以使用它。

Last step would be to make 226bit random value by calling several (3 of 4) times BD shuffled rng and combine 64bit values (and potential carry over) to make 226 random bits and then convert them to the deck shuffle.最后一步是通过调用数次(4 次 3 次)BD shuffled rng 并组合 64 位值(和潜在的结转)来生成 226 位随机值,以生成 226 位随机值,然后将它们转换为甲板洗牌。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM