在一个范围内生成无偏随机整数的最佳算法是什么?

时间:2012-08-01 12:05:04

标签: c++ c random uniform

在这个StackOverflow问题中:

Generating random integer from a range

接受的答案建议使用以下公式生成给定minmax之间的随机整数,其中minmax包含在范围内:

output = min + (rand() % (int)(max - min + 1))

但它也说

  

这仍然稍微偏向较低的数字......它也是   可以扩展它以消除偏见。

但这并没有解释为什么它偏向较低的数字或如何消除偏见。所以,问题是:这是在(签名)范围内生成随机整数的最佳方法,而不依赖于任何花哨的东西,只有rand()函数,如果它是最优的,如何删除偏见?

修改

我刚刚测试了@Joey针对浮点外推建议的while - 循环算法:

static const double s_invRandMax = 1.0/((double)RAND_MAX + 1.0);
return min + (int)(((double)(max + 1 - min))*rand()*s_invRandMax);

看看有多少统一的“球”正在“落入”并分布在许多“桶”中,一个用于浮点外推,另一个用于while - 环算法。但结果根据“球”(和“桶”)的数量而变化,所以我不能轻易选择胜利者。工作代码可以在this Ideone page找到。例如,对于10个桶和100个球,对于浮点外推,桶中理想概率的最大偏差小于while - 环算法(分别为0.04和0.05),但有1000个球,最大值while - 循环算法的偏差较小(0.024和0.011),而对于10000个球,浮点外推再次做得更好(0.0034和0.0053),依此类推,没有太多的一致性。考虑到没有一种算法能够始终如一地产生比其他算法更好的均匀分布的可能性,这使我倾向于浮点外推,因为它似乎比while - 循环算法执行得更快。那么选择浮点外推算法还是我的测试/结论不是完全正确的呢?

7 个答案:

答案 0 :(得分:14)

问题在于你正在进行模运算。如果RAND_MAX可以被你的模数整除,那就不会有问题,但通常情况并非如此。作为一个非常人为的例子,假设RAND_MAX为11,你的模数为3.你将获得以下可能的随机数和以下结果:

0 1 2 3 4 5 6 7 8 9 10
0 1 2 0 1 2 0 1 2 0 1

如您所见,0和1的可能性略高于2。

解决此问题的一个选择是拒绝抽样:通过禁用上面的数字9和10,您可以使得到的分布再次均匀。棘手的部分是弄清楚如何有效地做到这一点。在Java的java.util.Random.nextInt(int)方法中可以找到一个非常好的例子(我花了两天时间来理解为什么它有效)。

Java的算法有点棘手的原因是它们避免了诸如乘法和除法之类的慢速操作。如果你不太在意,你也可以用天真的方式做到这一点:

int n = (int)(max - min + 1);
int remainder = RAND_MAX % n;
int x, output;
do {
  x = rand();
  output = x % n;
} while (x >= RAND_MAX - remainder);
return min + output;

编辑:更正了上述代码中的fencepost错误,现在它可以正常工作。我还创建了一个小样本程序(C#;为0到15之间的数字采用统一的PRNG,并通过各种方式从中为0到6之间的数字构建PRNG):

using System;

class Rand {
    static Random r = new Random();

    static int Rand16() {
        return r.Next(16);
    }

    static int Rand7Naive() {
        return Rand16() % 7;
    }

    static int Rand7Float() {
        return (int)(Rand16() / 16.0 * 7);
    }

    // corrected
    static int Rand7RejectionNaive() {
        int n = 7, remainder = 16 % n, x, output;
        do {
            x = Rand16();
            output = x % n;
        } while (x >= 16 - remainder);
        return output;
    }

    // adapted to fit the constraints of this example
    static int Rand7RejectionJava() {
        int n = 7, x, output;
        do {
            x = Rand16();
            output = x % n;
        } while (x - output + 6 > 15);
        return output;
    }

    static void Test(Func<int> rand, string name) {
        var buckets = new int[7];
        for (int i = 0; i < 10000000; i++) buckets[rand()]++;
        Console.WriteLine(name);
        for (int i = 0; i < 7; i++) Console.WriteLine("{0}\t{1}", i, buckets[i]);
    }

    static void Main() {
        Test(Rand7Naive, "Rand7Naive");
        Test(Rand7Float, "Rand7Float");
        Test(Rand7RejectionNaive, "Rand7RejectionNaive");
    }
}

结果如下(粘贴到Excel中并添加单元格的条件着色以使差异更明显):

enter image description here

现在我在上面的拒绝取样中修正了我的错误,它应该正常工作(在它偏向0之前)。正如您所看到的,浮动方法根本不完美,它只是以不同方式分配偏差数字。

答案 1 :(得分:11)

当随机数发生器(RAND_MAX + 1)的输出数量不能被所需范围(max-min + 1)整除时,会出现问题。由于从随机数到输出将存在一致的映射,因此某些输出将映射到比其他输出更多的随机数。这与映射的完成无关 - 你可以使用模数,除法,转换到浮点,无论你能提出什么伏都,基本问题仍然存在。

问题的严重程度非常小,而且要求不高的应用程序通常可以忽略它。范围越小,RAND_MAX越大,效果就越不明显。

我拿了你的示例程序并调整了一下。首先,我创建了一个rand的特殊版本,其范围仅为0-255,以更好地展示效果。我对rangeRandomAlg2进行了一些调整。最后,我更改了#34;球的数量&#34;以1000000来提高一致性。您可以在此处查看结果:http://ideone.com/4P4HY

请注意,浮点版本产生两个紧密分组的概率,接近0.101或0.097,两者之间没有任何内容。这是行动中的偏见。

我认为称之为&#34; Java&#34;算法&#34;有点误导 - 我确定它比Java早。

int rangeRandomAlg2 (int min, int max)
{
    int n = max - min + 1;
    int remainder = RAND_MAX % n;
    int x;
    do
    {
        x = rand();
    } while (x >= RAND_MAX - remainder);
    return min + x % n;
}

答案 2 :(得分:6)

很容易理解为什么这个算法产生有偏差的样本。假设您的rand()函数返回集合{0, 1, 2, 3, 4}中的统一整数。如果我想使用它来生成随机位01,我会说rand() % 2。集合{0, 2, 4}为我提供了0,集合{1, 3}为我提供了1 - 所以我清楚地将0与60%和1进行了抽样有40%的可能性,根本不统一!

要解决此问题,您必须确保所需范围除以随机数生成器的范围,或者当随机数生成器返回大于最大值的数字时,丢弃结果可能是目标范围的倍数。

在上面的示例中,目标范围是2,适合随机生成范围的最大倍数是4,因此我们丢弃任何不在集合{0, 1, 2, 3}中的样本并再次滚动。

答案 3 :(得分:3)

到目前为止,最简单的解决方案是std::uniform_int_distribution<int>(min, max)

答案 4 :(得分:1)

在不失一般性的情况下,可以将在[a,b]上生成随机整数的问题简化为在[0,s)上生成随机整数的问题。以下最新出版物代表了从统一PRNG生成有界范围内的随机整数的技术现状:

Daniel Lemire,“间隔中的快速随机整数生成”。 ACM Trans。模型。计算Simul。 29,1,第3条(2019年1月)(ArXiv draft

Lemire表明,他的算法提供了公正的结果,并且受到诸如Melissa O'Neill的PCG generators之类的非常快速的高质量PRNG日益普及的启发,展示了如何快速计算结果,避免了缓慢除法几乎所有时间都在运作。

他的算法的示例ISO-C实现在下面的randint()中显示。在这里,我结合乔治·玛格萨利亚(George Marsaglia)的老KISS64 PRNG进行了演示。出于性能原因,通常最好通过机器特定的内在函数或直接映射到适当硬件指令的内联汇编来实现所需的64×64→128位无符号乘法。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

/* PRNG state */
typedef struct Prng_T *Prng_T;
/* Returns uniformly distributed integers in [0, 2**64-1] */
uint64_t random64 (Prng_T);
/* Multiplies two 64-bit factors into a 128-bit product */
void umul64wide (uint64_t, uint64_t, uint64_t *, uint64_t *);

/* Generate in bias-free manner a random integer in [0, s) with Lemire's fast
   algorithm that uses integer division only rarely. s must be in [0, 2**64-1].

   Daniel Lemire, "Fast Random Integer Generation in an Interval," ACM Trans.
   Model. Comput. Simul. 29, 1, Article 3 (January 2019)
*/
uint64_t randint (Prng_T prng, uint64_t s) 
{
    uint64_t x, h, l, t;
    x = random64 (prng);
    umul64wide (x, s, &h, &l);
    if (l < s) {
        t = (0 - s) % s;
        while (l < t) {
            x = random64 (prng);
            umul64wide (x, s, &h, &l);
        }
    }
    return h;
}

#define X86_INLINE_ASM (0)

/* Multiply two 64-bit unsigned integers into a 128 bit unsined product. Return
   the least significant 64 bist of the product to the location pointed to by
   lo, and the most signfiicant 64 bits of the product to the location pointed
   to by hi.
*/
void umul64wide (uint64_t a, uint64_t b, uint64_t *hi, uint64_t *lo)
{
#if X86_INLINE_ASM
    uint64_t l, h;
    __asm__ (
        "movq  %2, %%rax;\n\t"  // rax = a
        "mulq  %3;\n\t"         // rdx:rax = a * b
        "movq  %%rax, %0;\n\t"  // l = (a * b)<31:0>
        "movq  %%rdx, %1;\n\t"  // h = (a * b)<63:32>
        : "=r"(l), "=r"(h)
        : "r"(a), "r"(b)
        : "%rax", "%rdx");
    *lo = l;
    *hi = h;
#else // X86_INLINE_ASM
    uint64_t a_lo = (uint64_t)(uint32_t)a;
    uint64_t a_hi = a >> 32;
    uint64_t b_lo = (uint64_t)(uint32_t)b;
    uint64_t b_hi = b >> 32;

    uint64_t p0 = a_lo * b_lo;
    uint64_t p1 = a_lo * b_hi;
    uint64_t p2 = a_hi * b_lo;
    uint64_t p3 = a_hi * b_hi;

    uint32_t cy = (uint32_t)(((p0 >> 32) + (uint32_t)p1 + (uint32_t)p2) >> 32);

    *lo = p0 + (p1 << 32) + (p2 << 32);
    *hi = p3 + (p1 >> 32) + (p2 >> 32) + cy;
#endif // X86_INLINE_ASM
}

/* George Marsaglia's KISS64 generator, posted to comp.lang.c on 28 Feb 2009
   https://groups.google.com/forum/#!original/comp.lang.c/qFv18ql_WlU/IK8KGZZFJx4J
*/
struct Prng_T {
    uint64_t x, c, y, z, t;
};

struct Prng_T kiss64 = {1234567890987654321ULL, 123456123456123456ULL,
                        362436362436362436ULL, 1066149217761810ULL, 0ULL};

/* KISS64 state equations */
#define MWC64 (kiss64->t = (kiss64->x << 58) + kiss64->c,            \
               kiss64->c = (kiss64->x >> 6), kiss64->x += kiss64->t, \
               kiss64->c += (kiss64->x < kiss64->t), kiss64->x)
#define XSH64 (kiss64->y ^= (kiss64->y << 13), kiss64->y ^= (kiss64->y >> 17), \
               kiss64->y ^= (kiss64->y << 43))
#define CNG64 (kiss64->z = 6906969069ULL * kiss64->z + 1234567ULL)
#define KISS64 (MWC64 + XSH64 + CNG64)
uint64_t random64 (Prng_T kiss64)
{
    return KISS64;
}

int main (void)
{
    int i;
    Prng_T state = &kiss64;

    for (i = 0; i < 1000; i++) {
        printf ("%llu\n", randint (state, 10));
    }
    return EXIT_SUCCESS;
}

答案 5 :(得分:0)

如果您真的想假设您拥有的rand()函数是一个完美的生成器,则需要使用下面介绍的方法。

我们将创建一个随机数r,从0到max-min = b-1,然后可以轻松地将其移动到所需的范围,只需取r + min

我们将创建一个随机数,其中b

步骤:

  1. 采用原始RAND_MAX大小的随机数r进行截断
  2. 在基数b中显示此数字
  3. 从0到b-1的m个随机数中,首先取该数字的m = floor(log_b(RAND_MAX))个数字
  4. 将每一个按min(即r + min)移动,以将其调整为所需的范围(min,max)

由于log_b(RAND_MAX)不一定是整数,因此浪费了表示中的最后一位。

仅使用mod(%)的原始方法被

误解了
(log_b(RAND_MAX) - floor(log_b(RAND_MAX)))/ceil(log_b(RAND_MAX)) 

您可能不会同意的那么多,但是如果您坚持要精确,那就是程序。

答案 6 :(得分:0)

您涉及到涉及随机整数算法的两点:是最优,还是无偏

最佳

有很多方法可以定义“最佳”算法。在这里,我们根据平均使用的随机比特数来看“最优”算法。从这个意义上讲,rand是用于随机数的一种较差的方法,部分原因是它不一定需要产生随机位(因为未正确指定RAND_MAX)*。取而代之的是,我们假设我们有一个“真实的”随机发生器,可以产生无偏且独立的随​​机位。

1976年,DE Knuth和AC Yao表明,任何仅使用随机位以给定概率生成随机整数的算法都可以表示为二叉树,其中随机位指示遍历树和每片叶子的方式(端点)对应于结果。他们还给出了给定算法平均需要多少位才能完成此任务的下限。在这种情况下,用于均匀地在[0, n)中生成整数的最优算法平均平均最多需要log2(n) + 2位。在这种意义上,有许多 optimized 算法的示例。其中之一是J. Lumbroso(2013)的Fast Dice Roller;下面显示了使用JavaScript而不是C或C ++的实现,但是很容易适应两种语言,其思想是表明可以以最佳方式从位生成整数。在代码中,(Math.random() < 0.5 ? 0 : 1)是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 ? 0 : 1)
    y = y * 2 + randomBit
    if(x > maxInclusive) {
      if (y <= maxInclusive) { return y + minInclusive }
      // Rejection
      x = x - maxInclusive - 1
      y = y - maxInclusive - 1
    }
  }
}

不偏不倚

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

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

,无论哪种情况,该算法都不会在恒定时间内运行,并且在最坏的情况下将永远运行。 (另一方面,当n为2的幂时,最佳的二叉树将具有有限的深度,并且没有拒绝节点。)快速骰子滚子是使用“拒绝”事件执行以下操作的算法示例:确保它没有偏见;参见上面代码中的注释。

对于一般的n,没有办法在不引入偏差的情况下“解决”这种最坏情况下的时间复杂性。例如,模减少(包括您问题中的min + (rand() % (int)(max - min + 1)))等同于二叉树,其中拒绝叶子被标记为结果-但是由于拒绝叶子的结果可能更多,因此只有部分结果可以代替拒绝叶子,引入偏见。如果您在设置一定数量的迭代后停止拒绝,则会产生相同类型的二叉树和相同类型的偏差。 (但是,根据应用的不同,这种偏见可以忽略不计。随机整数生成还存在安全方面的问题,这些问题太复杂了,无法在此答案中讨论。)

注意

*也有other problems with rand()。也许最严重的事实是C标准没有为rand()返回的数字指定特定的分布。