如何从随机位流中生成[0,n]范围内的随机整数而不浪费位?

时间:2011-05-18 15:05:03

标签: random integer uniform

我有一个(均匀)随机位流,我希望在[0,n]范围内统一生成随机整数,而不会浪费比特。 (我正在考虑超出楼层(log_2(n))+ 1的比特浪费,假设它总是可以使用不超过它。)例如,如果n = 5,那么算法我是寻找应该使用不超过三位。怎么办呢?

5 个答案:

答案 0 :(得分:5)

让我说说随机整数生成算法,就其平均使用的随机位数而言,它们是“最佳”的。在本文的其余部分,我们将假定我们有一个“真实的”随机生成器,该生成器可以生成无偏且独立的随​​机位。

1976年,DE Knuth和AC Yao表明,任何仅使用随机位以给定概率生成随机整数的算法都可以表示为二叉树,其中随机位指示遍历树和每片叶子的方式(端点)对应于结果。 Knuth和Yao指出,任何用于均匀地在[0, n)中生成整数的最优二叉树算法,都需要至少 log2(n)位和最多log2(n) + 2位平均。 (因此,即使 optimum 算法也有可能“浪费”位。)有关最佳算法的示例,请参见下文。

但是,任何最优整数生成器(也都是无偏)通常会在最坏的情况下永远运行,如Knuth和Yao所示。回到二叉树,n个结果标签中的每一个都留在二叉树中,以便[0,n)中的每个整数都可以1 / n的概率出现。但是,如果1 / n具有不间断的二进制扩展(如果n不是2的幂,就会是这种情况),那么该二进制树必然是其中一个-

  • 具有“无限”的深度,或者
  • 在树的末端
  • 包括“拒绝”叶子,

无论哪种情况,即使平均使用很少的随机位,该算法也将在最坏的情况下永远运行。 (另一方面,当n为2的幂时,最佳二叉树将没有拒绝节点,并且在返回结果之前恰好需要n位,因此不会浪费任何位。)快速骰子滚子是使用“拒绝”事件以确保其无偏的算法示例;请参阅下面的代码中的注释。

因此,通常,随机整数生成器可以是 无偏的恒定时间(或什至都不是),。 >并且二叉树概念表明,一般而言,没有办法在不引入偏差的情况下“修复”不确定运行时间的最坏情况。例如,模减少(例如rand() % n)等效于二叉树,其中拒绝叶子用标记的结果替换-但是由于拒绝叶子的可能性更大,因此只有部分结果可以代替拒绝叶子,造成偏见。如果您在设置一定数量的迭代后停止拒绝,则会产生相同类型的二叉树和相同类型的偏差。 (但是,根据应用的不同,这种偏见可以忽略不计。随机整数生成还存在安全方面的问题,这些问题太复杂了,无法在此答案中讨论。)

快速骰子滚子实现

在前面给出的意义上,有很多 optimized 算法的示例。其中之一是J. Lumbroso(2013)的Fast Dice Roller(在下面实现),也许其他示例是此处其他答案之一中给出的算法以及2004年Math Forum中给出的算法另一方面,所有算法surveyed by M. O'Neill都不是最优的,因为它们一次依赖于生成随机位的块。另请参阅我在integer generating algorithms上的笔记。

以下是Fast Dice Roller的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
    }
 }
}

减少钻头浪费

回想一下,“最优”整数生成器(例如上面的Fast Dice Roller)平均平均至少使用log2(n)位(下限),或者平均使用此下限的2位。可以使用多种技术来使算法(甚至不是最佳算法)更接近此理论下限,包括批处理和随机性提取。在以下内容中进行了讨论:

答案 1 :(得分:3)

这相当于在两组不同(有限)基数之间找到双向函数。这是不可能的。

答案 2 :(得分:1)

虽然您的问题描述指定了每个随机数生成的固定位数,但您的标题却没有。所以我要在此处添加平均,您可以生成一个随机数,其中包含您所声明的位数加半个位。下面的算法对n的值不能被2整除的值采用可变位数,但是它将消耗的平均数位是 floor(log_2(n))+ 1.5

用于生成范围内的整数的函数的标准实现在大型随机数上使用%(modulo)。这会浪费比特,并且不会产生数学上精确的随机分布,除非重新运行大随机数的某些值。以下算法产生真正的随机分布,不会浪费比特。 (或者我没有看到一种明显的方法来减少它消耗的位数。也许一些熵可以从“数量太大”的情况中恢复。)

# Generate a number from 0 to n inclusive without wasting bits.
function RandomInteger(n)
    if n <= 0
        error
    else
        i = Floor(Log2(n))
        x = i
        r = 0
        while x >= 0
            r = r + (2 ^ x) * NextRandomBit()
            if r > n 
                # Selected number too large so begin again.
                x = i 
                r = 0
            else
                # Still in range. Calculate the next bit.
                x = x - 1
        return r

上面的算法是为了清晰而不是速度而编写的。如果重写一次处理多个位,那将会非常快。

答案 3 :(得分:0)

您似乎可以一次取x = ceil(log_2(n))位,并将它们用作随机数。您遇到的问题是,如果您收到的数字大于您的限制(例如5),那么您将需要执行一些魔术来使其小于5,但是要均匀。在这种情况下,似乎合乎逻辑的是你只需要另外x位,但由于你已经指定我们不能浪费比特,那么我们必须更具创造性。我建议左右旋转,但这并不总能让你摆脱这种情况。 (当你想要n = 5时,考虑一个111的字符串)。我们可以进行x旋转,看看其中一个旋转是否使我们进入正确的范围,或者我们可以翻转所有位并添加1(二进制补码)。我相信这会使它变得统一。

所以,例如,如果您有以下字符串(最右边的位是您收到的第一个字符串):

101001111010010101

你使用的是n = 5,然后是ceil(log2(n))= 3,所以你一次要使用三位,以下是你的结果(每个时间步):

t=0 : 101 = 5
t=1: 010 = 2
t=2: 010 = 2
t=3: 111 = 7 -> too large, rotates won't work, so we use 2's complement: 001 = 1
t=4: 001 = 1
t=5: 101 = 5

答案 4 :(得分:0)

首先找出要生成的可能值的数量。如果整数在0..5范围内,则为6个值。它们可以用ceil(log(6)/ log(2))位表示。

// in C++
std::bitset< 3 > bits;
// fill the bitset

// interpret as a number
long value = bits.to_ulong();

然后找到从n位到最终表示格式的转换:它需要从范围[0..2 N ]缩放到范围[from,to]:

double out_from=-1, out_to=5;
double in_from=0, in_to = std::bitset<3>().flip().to_ulong();

double factor   = (out_to-out_from)/(in_to-in_from)
double constant = out_from - in_from;

double rescaled = in_value * scale + constant;
long out = floor( rescaled );