【问题标题】:How to generate a random integer in the range [0,n] from a stream of random bits without wasting bits?如何从随机位流中生成 [0,n] 范围内的随机整数而不浪费位?
【发布时间】:2011-05-18 15:05:02
【问题描述】:

我有一个(统一的)随机位流,我想从中均匀地生成 [0,n] 范围内的随机整数,而不会浪费位。 (我正在考虑超过 floor(log_2(n))+1 的比特浪费,假设总是可以使用不超过这个值。)例如,如果 n = 5,那么我的算法寻找应该使用不超过三位。如何做到这一点?

【问题讨论】:

  • 你确定可以这样做吗?
  • 我既没有证据表明它是可能的,也没有证据表明它是不可能的。如果 ceil(log_2(n)) bits 不是最小上界,那么需要的位数还有一些其他上界;无论最低限度如何,我都不想超过。
  • 如果n 不是 2 的幂,我怀疑是否有可能(接收具有 固定 位数的均匀分布)。可能有一些算法可以引导你到达那里速度更快(位数更少),但我怀疑是否能找到一种算法能够以固定数量的位数来完成。
  • 我的直觉是,对于 n 不是 2 的幂,你根本无法满足二进制计算机上的一致性要求,尽管我想你可以通过使用更多的位得到任意小的错误。
  • 是的,如果你真的想使用固定数量的比特,那么你可以考虑尽量减少均匀性错误。

标签: random


【解决方案1】:

让我谈谈在平均使用的随机位数方面“最佳”的随机整数生成算法。在本文的其余部分,我们将假设我们有一个“真正的”随机生成器,它可以产生无偏且独立的随​​机位。

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 位范围内。有多种技术可用于使算法(甚至不是最佳算法)更接近这个理论下限,包括批处理和随机性提取。这些讨论在:

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 是的,这是我骑车回家时发生的。我现在觉得有点傻。
    【解决方案3】:

    尽管您的问题描述指定了每个生成的随机数的固定位数,但您的标题却没有。所以我要在这里补充一点,平均而言,您可以生成一个随机数,其中包含您声明的位数加上半位。下面的算法对不可被 2 整除的 n 值采用可变位数,但它将消耗的平均位数为 floor(log_2(n)) + 1.5

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

    # 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
    

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

    【讨论】:

    • 如果我没看错,当你生成一个 r > n 时,你会重新开始。这让我不清楚你是如何达到平均消耗的比特数的,因为每个有限次数的重启都有非零概率。这是否恰好是一个收敛的序列,所以你可以总结它?
    • @uckelman 如果我们将 n 转换为二进制数,它将有 Floor(Log2(n))+1 位,最高位为 1。我们的第一个随机位不能生成一个太大的。如果第一个随机位是 0,那么任何后续的随机位序列都不会太大。所以第一个比特有 50% 的机会阻止任何比特的浪费。如果第一个随机位是 1,我们需要检查下一位。然后有 50% 的机会 n 中的下一位为 1,因为我们正在计算 n 的所有值,另外 50% 的机会没有浪费位。类似的东西。
    【解决方案4】:

    您似乎可以一次只取 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
    

    【讨论】:

    • 这不符合均匀性要求:对于 n = 5,p(1) = 1/4,而它应该是 1/6。
    【解决方案5】:

    首先找出您想要生成的可能值的数量。如果是 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..2N]范围缩放到[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 );
    

    【讨论】:

    • 这不符合一致性要求:对于 n = 5,p(3) = 1/4,而它应该是 1/6。
    • 随机化...从来没有看起来那么容易:(
    猜你喜欢
    • 1970-01-01
    • 2010-10-20
    • 2020-10-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-19
    • 1970-01-01
    • 1970-01-01
    • 2011-10-20
    相关资源
    最近更新 更多