【问题标题】:Bit-wise efficient uniform random number generation按位有效的均匀随机数生成
【发布时间】:2013-09-22 05:40:21
【问题描述】:

我记得在面向数学的网站上的一篇文章中读到了一种有效使用随机位的方法,但我似乎无法在 Google 中找到正确的关键字来找到它,而且它不在我的浏览器历史记录中。

所问问题的要点是在域 [domainStart, domainEnd) 中获取一个随机数序列,并有效地使用随机数序列的位均匀地投影到范围 [@ 987654323@,rangeEnd)。域和范围都是整数(更准确地说,longs 而不是 Z)。 执行此操作的算法是什么?

在实现方面,我有一个带有这个签名的函数:

long doRead(InputStream in, long rangeStart, long rangeEnd);

in 基于我需要使用的 CSPRNG(由硬件 RNG 提供,通过 SecureRandom 调节);返回值必须在rangeStartrangeEnd 之间,但明显的实现是浪费:

long doRead(InputStream in, long rangeStart, long rangeEnd) {
    long retVal = 0;
    long range = rangeEnd - rangeStart;

    // Fill until we get to range
    for (int i = 0; (1 << (8 * i)) < range; i++) {
        int in = 0;
        do {
            in = in.read();
        // but be sure we don't exceed range
        } while(retVal + (in << (8 * i)) >= range);
        retVal += in << (8 * i);
     }

    return retVal + rangeStart;
}

我相信这实际上与(rand() * (max - min)) + min 的想法相同,只是我们丢弃了将我们推向max 的位。我们没有使用可能会错误地将结果偏向较低值的模运算符,而是丢弃这些位并重试。由于击中 CSPRNG 可能会触发重新播种(这可能会阻止 InputStream),我想避免浪费随机位。 Henry 指出该代码偏向于 0 和 257; Banthar 在一个例子中进行了演示。

第一次编辑:亨利提醒我求和调用了中心极限定理。我已经修复了上面的代码来解决这个问题。

第二次编辑:机械蜗牛建议我查看 Random.nextInt() 的源代码。看了一会,才发现这个问题和base转换问题差不多。请参阅下面的答案。

【问题讨论】:

  • 您的“明显实现”不仅浪费而且在概念上也是错误的(除了一些实现错误)。通过添加随机数,您可以更改分布。如果添加了足够的数字,它将变为高斯。例如,掷两个骰子产生 7 的频率要比 2 高得多。
  • 谢谢。我知道我在算法上做了一些非常错误的事情。 :S 我应该可以睡一觉了。
  • java.util.Random.nextInt的实现。
  • @Banthar 该问题已在此期间进行了编辑;-) 现在可以了。
  • 由于最后一个字节的选择方式,还是有一点偏差。

标签: java random entropy


【解决方案1】:

您的算法会产生有偏差的结果。让我们假设rangeStart=0rangeEnd=257。如果第一个字节大于0,这将是结果。如果是0,则结果将是0256,概率为50/50。因此,0256 被选中的可能性比任何其他数字低两倍。

我做了一个简单的test 来确认这一点:

p(0)=0.001945
p(1)=0.003827
p(2)=0.003818
...
p(254)=0.003941
p(255)=0.003817
p(256)=0.001955

我认为您需要做与java.util.Random.nextInt 相同的操作并丢弃整个数字,而不是仅丢弃最后一个字节。

【讨论】:

  • 正确,为了减少超出范围的情况,可以只取必要的位而不是完整的字节。例如,要获取 [0..700) 中的数字,只需占用 10 位而不是两个字节,如果 >= 700 则丢弃。
【解决方案2】:

看了 Random.nextInt() 的源码后,我意识到这个问题和基数转换问题类似。

与一次转换单个符号相比,通过累加器“缓冲区”一次转换输入符号块会更有效,该缓冲区大到足以表示域中和范围内的至少一个符号.新代码如下所示:

public int[] fromStream(InputStream input, int length, int rangeLow, int rangeHigh) throws IOException {
    int[] outputBuffer = new int[length];
    // buffer is initially 0, so there is only 1 possible state it can be in
    int numStates = 1;
    long buffer = 0;
    int alphaLength = rangeLow - rangeHigh;
    // Fill outputBuffer from 0 to length
    for (int i = 0; i < length; i++) {
        // Until buffer has sufficient data filled in from input to emit one symbol in the output alphabet, fill buffer.
        fill:
        while(numStates < alphaLength) {
            // Shift buffer by 8 (*256) to mix in new data (of 8 bits)
            buffer = buffer << 8 | input.read();
            // Multiply by 256, as that's the number of states that we have possibly introduced
            numStates = numStates << 8;
        }
        // spits out least significant symbol in alphaLength
        outputBuffer[i] = (int) (rangeLow + (buffer % alphaLength));
        // We have consumed the least significant portion of the input.
        buffer = buffer / alphaLength;
        // Track the number of states we've introduced into buffer
        numStates = numStates / alphaLength;
    }
    return outputBuffer;
}

然而,在基数之间转换数字和这个问题之间存在根本区别;为了在基数之间进行转换,我认为需要有足够的关于数字的信息来执行计算 - 目标基数的连续除法会产生用于构造目标字母表中数字的余数。在这个问题中,我真的不需要知道所有这些信息,只要我不偏向数据,这意味着我可以做我在标记为“填充”的循环中所做的事情。

【讨论】:

  • 我开始意识到有一些条件使这个问题无法解决。我稍后会编辑此答案以说明这一点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-07
  • 2010-12-04
  • 1970-01-01
  • 2017-07-16
  • 1970-01-01
相关资源
最近更新 更多