【问题标题】:A more randomness-efficent way to generate random integers一种更随机有效的生成随机整数的方法
【发布时间】:2014-08-04 02:37:23
【问题描述】:

我有一个应用程序,我需要测量算法消耗多少位随机性。我已经检测了Random 的一个子类,通过覆盖Random.next(int) 在调用其父方法之前增加一个计数器来执行此操作。

我在实现nextInt(int) 方法时遇到了一些问题,因为即使范围是2 的幂,它也总是会绘制32 位。对于其他范围,问题就更大了:方法不统一---只重试一次,只针对绘制的原始值大于小于Integer.MAX_VALUE的范围的最大倍数的情况--- 而且它仍然使用了比需要更多的随机性。

如何实现更好的nextInt(int) 版本,它只使用确定范围内的值所需的最小随机位数,同时完全一致?它不需要保证终止(无论如何不可能),只需以概率 1 终止。

编辑:

这是我目前所拥有的:

int nextInt(int max){
    int n = Integer.numberOfTrailingZeros(max);
    return next(n) + nextOddInteger(max >> n) << n;
}

这可能不完全正确,但基本上这会从num 中排除所有n 二进制数,生成带有n 位的随机数,并将nextOddInteger(num) 添加到结果位。 nextOddInteger 将生成一个随机整数,最大为素数分解不包含二的数字。如何以非常随机高效的方式实现这部分?

【问题讨论】:

  • Java 的 Random 类只是一个 LCPRNG,例如可用的最基本类型。在 *NIX 上,您可以直接从 /dev/random 中提取位,但我不建议将它用于跨平台 java。
  • @RichardJ.RossIII 目前,底层 RNG 的质量不是问题,只是以均匀随机方式选择整数所需的位数(假设它们很好) .我最终将替换底层的 RNG,但我首先在这部分工作。

标签: java random


【解决方案1】:

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

1976 年,D. E. Knuth 和 A. C. Yao 表明,任何仅使用随机位以给定概率产生随机整数的算法都可以表示为二叉树,其中随机位指示遍历树和每个叶子的方式(端点)对应于一个结果。他们还给出了给定算法执行此任务所需的平均位数的下限。在这种情况下,在[0, n) 中统一生成整数的最佳 算法平均最多需要log2(n) + 2 位。在这个意义上,有很多最优算法的例子。其中一个是 J. Lumbroso (2013) 的 Fast Dice Roller(在下面实现),另一个可能是 2004 年 Math Forum 中给出的算法。另一方面,所有算法 surveyed by M. O'Neill 都不是最优的,因为它们依赖于一次生成随机位块。

然而,任何最优整数生成器也是无偏的,一般来说,在最坏的情况下会永远运行,正如 Knuth 和 Yao 所展示的那样。回到二叉树,n 个结果标签中的每一个都留在二叉树中,因此 [0, n) 中的每个整数都可以以 1/n 的概率出现。但是如果 1/n 有一个非终止的二元展开式(如果 n 不是 2 的幂就是这种情况),这棵二叉树必然要么——

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

在任何一种情况下,算法都会在最坏的情况下永远运行,即使它平均使用很少的随机位。 (另一方面,当 n 是 2 的幂时,最优二叉树将具有有限深度且没有拒绝节点。)快速骰子滚轮是使用“拒绝”事件来确保其无偏性的算法示例;请参阅下面代码中的注释。

因此,一般而言,随机整数生成器可以是任一无偏恒定时间(甚至两者都不是),但不能两者兼而有之。 特别是,通常没有办法在不引入偏差的情况下“修复”不确定运行时间的最坏情况。例如,模约简(例如,randInt() % n)相当于一棵二叉树,其中拒绝叶被标记的结果替换——但由于可能的结果比拒绝叶多,因此只有一些结果可以代替拒绝离开,引入偏见。如果您在一定次数的迭代后停止拒绝,则会产生相同类型的二叉树 - 以及相同类型的偏差。 (但是,根据应用程序,这种偏差可能可以忽略不计。随机整数生成也有安全方面的问题,这太复杂了,无法在此答案中讨论。)

请注意,我们假设我们有一个随机位生成器。但是,Java 实现中存在一个问题:它无法生成单独的随机位。 (事实上​​,大多数伪随机数生成器一次生成位块,而不是单个位。)因此,正如在撰写本文时的另一个答案中所提到的,为了优化使用随机位,您需要保存结果nextInt() 并一次读出一个位。读取完所有位后,生成另一个 nextInt() 并重复。

快速掷骰子实现

以下是快速掷骰子的 JavaScript 实现。请注意,它使用拒绝事件和循环来确保它是公正的。 nextBit() 是一种产生独立无偏随机位的方法(例如,Math.random()&lt;0.5 ? 1 : 0,在 Java 或 JavaScript 中最终依赖的随机位方面不一定有效)。

function randomInt(minInclusive, maxExclusive) {
 var maxInclusive = (maxExclusive - minInclusive) - 1
 var x = 1
 var y = 0
 while(true) {
    x = x * 2
    var randomBit = Math.random()<0.5 ? 1 : 0
    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)
    }
 }
}

【讨论】:

    【解决方案2】:

    听起来您应该绘制所有位,将它们放入持久队列中,并根据需要通过弹出位来消耗它们。仅在用完时才绘制新的随机位。

    例如如果 Random.next 绘制 32 位并且您需要 8 位,那么您只需为每四个需要 8 位的调用绘制一次。

    【讨论】:

    • 这里的问题不是next(int)在输出前丢掉的位数,而是nextInt(int)消耗的位数。
    • @AJMansfield 但是下次你调用 nextInt 时,它根本不需要绘制,因为剩余的空间已经足够了。
    • 如果您阅读文档,next(int) 已经提供了这一点。您指定所需的位数作为函数的参数。
    • @AJMansfield 那么我不明白你的问题,至少第一个问题是 2-even 范围过度消耗。
    • 我在我的问题中添加了更多信息,可以解释问题所在。
    猜你喜欢
    • 2015-12-19
    • 2018-01-10
    • 2013-05-13
    • 2013-03-26
    • 2023-03-22
    • 2019-10-29
    • 1970-01-01
    • 2015-11-21
    • 1970-01-01
    相关资源
    最近更新 更多