嗯,你肯定需要 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。