让我谈谈在平均使用的随机位数方面“最佳”的随机整数生成算法。在本文的其余部分,我们将假设我们有一个“真正的”随机生成器,它可以产生无偏且独立的随机位。
1976 年,D. E. Knuth 和 A. C. Yao 表明,任何仅使用随机位以给定概率产生随机整数的算法都可以表示为二叉树,其中随机位指示遍历树和每个叶子的方式(端点)对应于一个结果。 (Knuth 和 Yao,“非均匀随机数生成的复杂性”,Algorithms and Complexity,1976 年。)Knuth 和 Yao 表明,任何用于生成整数的最优二叉树算法在[0, n) 中,平均需要至少log2(n) 和最多log2(n) + 2 位。 (因此,即使是 最佳 算法也有可能“浪费”比特。)请参阅下面的最佳算法示例。
然而,任何最优整数生成器也是无偏的,一般来说,在最坏的情况下会永远运行,正如 Knuth 和 Yao 所展示的那样。回到二叉树,n 个结果标签中的每一个都留在二叉树中,因此 [0, n) 中的每个整数都可以以 1/n 的概率出现。但是如果 1/n 有一个非终止的二元展开式(如果 n 不是 2 的幂就是这种情况),这棵二叉树必然要么——
- 具有“无限”深度,或
- 在树的末端包含“拒绝”叶子,
在任何一种情况下,算法都会在最坏的情况下永远运行,即使它平均使用很少的随机位。 (另一方面,当 n 是 2 的幂时,最优二叉树将没有拒绝节点,并且在返回结果之前恰好需要 n 位,因此不会“浪费”任何位。)快速掷骰子是一种使用“拒绝”事件来确保其无偏见的算法示例;请参阅下面代码中的注释。
因此,一般而言,随机整数生成器可以是任一无偏或恒定时间(甚至两者都不是),但不能两者兼而有之。 二叉树的概念表明,一般来说,在不引入偏差的情况下,没有办法“修复”不确定运行时间的最坏情况。例如,模约简(例如,rand() % n)相当于一棵二叉树,其中拒绝叶被标记的结果替换——但由于可能的结果比拒绝叶多,因此只有一些结果可以代替拒绝离开,引入偏见。如果您在一定次数的迭代后停止拒绝,则会产生相同类型的二叉树 - 以及相同类型的偏差。 (但是,根据应用程序,这种偏差可能可以忽略不计。随机整数生成也有安全方面的问题,这太复杂了,无法在此答案中讨论。)
快速掷骰子实现
在前面给出的意义上,有很多最优算法的例子。其中之一是 J. Lumbroso (2013) 的Fast Dice Roller(在下面实现),也许其他示例是此处其他答案之一中给出的算法以及 2004 年Math Forum 中给出的算法。另一方面手,所有算法surveyed by M. O'Neill 都不是最优的,因为它们依赖于一次生成随机位块。另请参阅我在 integer generating algorithms 上的说明。
以下是快速掷骰子的 JavaScript 实现。请注意,它使用拒绝事件和循环来确保它是公正的。 nextBit() 是一种产生独立无偏随机位的方法(例如,Math.random()<0.5 ? 1 : 0,就 JavaScript 中最终依赖的随机位而言,它不一定有效)。
function randomInt(minInclusive, maxExclusive) {
var maxInclusive = (maxExclusive - minInclusive) - 1
var x = 1
var y = 0
while(true) {
x = x * 2
var randomBit = nextBit()
y = y * 2 + randomBit
if(x > maxInclusive) {
if (y <= maxInclusive) { return y + minInclusive }
// Rejection
x = x - maxInclusive - 1
y = y - maxInclusive - 1
}
}
}
以下版本返回 BigInt,这是最新版本的 JavaScript 支持的任意精度整数:
function randomInt(minInclusive, maxExclusive) {
minInclusive=BigInt(minInclusive)
maxExclusive=BigInt(maxExclusive)
var maxInclusive = (maxExclusive - minInclusive) - BigInt(1)
var x = BigInt(1)
var y = BigInt(0)
while(true) {
x = x * BigInt(2)
var randomBit = BigInt(Math.random()<0.5 ? 1 : 0)
y = y * BigInt(2) + randomBit
if(x > maxInclusive) {
if (y <= maxInclusive) { return y + minInclusive }
// Rejection
x = x - maxInclusive - BigInt(1)
y = y - maxInclusive - BigInt(1)
}
}
}
减少钻头浪费
回想一下,“最佳”整数生成器(例如上面的快速骰子滚轮)平均使用至少 log2(n) 位(下限),或者平均在该下限的 2 位范围内。有多种技术可用于使算法(甚至不是最佳算法)更接近这个理论下限,包括批处理和随机性提取。这些讨论在: