为什么rand()%6有偏见?

时间:2018-04-17 13:04:33

标签: c++ random std

在阅读如何使用std :: rand时,我在cppreference.com

上找到了这段代码
int x = 7;
while(x > 6) 
    x = 1 + std::rand()/((RAND_MAX + 1u)/6);  // Note: 1+rand()%6 is biased

右边的表达有什么问题?尝试过,它完美无缺。

4 个答案:

答案 0 :(得分:136)

rand() % 6存在两个问题(1+不会影响这两个问题。)

首先,正如几个答案所指出的那样,如果rand()的低位不均匀,则余数运算符的结果也不均匀。

其次,如果rand()产生的不同值的数量不是6的倍数,则余数将产生比高值更低的值。即使rand()返回完美分布的值,也是如此。

作为一个极端的例子,假装rand()[0..6]范围内产生均匀分布的值。如果您查看这些值的余数,当rand()返回范围[0..5]中的值时,余数会在[0..5]范围内生成均匀分布的结果。当rand()返回6时,rand() % 6返回0,就像rand()返回0一样。所以你得到的分布是任何其他值的两倍0。

第二个是rand() % 6真实问题。

避免此问题的方法是丢弃会产生非均匀重复的值。你计算6的最大倍数小于或等于RAND_MAX,并且每当rand()返回一个大于或等于该倍数的值时你就拒绝它并再次调用`rand()需要的时间。

所以:

int max = 6 * ((RAND_MAX + 1u) / 6)
int value = rand();
while (value >= max)
    value = rand();

这是对相关代码的不同实现,旨在更清楚地显示正在发生的事情。

答案 1 :(得分:19)

这里有隐藏的深度:

  1. u中使用小RAND_MAX + 1uRAND_MAX被定义为int类型,通常是最大的int。在RAND_MAX + 1类型溢出的情况下,signed的行为将未定义。写1u强制将RAND_MAX类型转换为unsigned,以避免溢出。

  2. % 6 的使用可以(但在std::rand的每个实现中,我看到)介绍任何超出所提出的替代方案的额外统计偏差。 % 6危险的情况是数字生成器具有低阶位相关平台的情况,例如rand中相当着名的IBM实现(在C中),我认为,1970年代将高位和低位翻转为“最后的繁荣”。进一步的考虑是6是非常小的参考。 RAND_MAX,如果RAND_MAX不是6的倍数,那么效果会很小,可能不是。

  3. 总之,最近,由于其易处理性,我使用% 6。除了发电机本身引入的统计异常之外,它不太可能引入任何统计异常。如果您仍然有疑问,测试您的生成器,看它是否具有适合您的用例的统计属性。

答案 2 :(得分:2)

我不是一个经验丰富的C ++用户,但有兴趣看看其他答案是否有关 std::rand()/((RAND_MAX + 1u)/6)的偏见低于1+std::rand()%6实际上是正确的。所以我写了一个测试程序来列出两种方法的结果(我已经很久没写过C ++了,请检查一下)。找到运行代码的链接here。它还转载如下:

// Example program
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <string>

int main()
{
    std::srand(std::time(nullptr)); // use current time as seed for random generator

    // Roll the die 6000000 times using the supposedly unbiased method and keep track of the results

    int results[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::rand()/((RAND_MAX + 1u)/6);  // Note: 1+rand()%6 is biased

        results[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results[n] << ' ';
    }

    std::cout << "\n";


    // Roll the die 6000000 times using the supposedly biased method and keep track of the results

    int results_bias[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::rand()%6;

        results_bias[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results_bias[n] << ' ';
    }
}

然后我接受了这个输出,并使用R中的chisq.test函数运行卡方检验,看看结果是否与预期的显着不同。这个stackexchange问​​题详细介绍了使用卡方检验测试模具公平性:How can I test whether a die is fair?。以下是几次运行的结果:

> ?chisq.test
> unbias <- c(100150, 99658, 100319, 99342, 100418, 100113)
> bias <- c(100049, 100040, 100091, 99966, 100188, 99666 )

> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 8.6168, df = 5, p-value = 0.1254

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 1.6034, df = 5, p-value = 0.9008

> unbias <- c(998630, 1001188, 998932, 1001048, 1000968, 999234 )
> bias <- c(1000071, 1000910, 999078, 1000080, 998786, 1001075   )
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.051, df = 5, p-value = 0.2169

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 4.319, df = 5, p-value = 0.5045

> unbias <- c(998630, 999010, 1000736, 999142, 1000631, 1001851)
> bias <- c(999803, 998651, 1000639, 1000735, 1000064,1000108)
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.9592, df = 5, p-value = 0.1585

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 2.8229, df = 5, p-value = 0.7273

在我做的三次运行中,两种方法的p值总是大于用于检验显着性的典型alpha值(0.05)。这意味着我们不会认为它们中的任何一个都有偏见。有趣的是,所谓的无偏差方法具有持续较低的p值,这表明它实际上可能更偏向。需要注意的是,我只做了3次。

更新:在我写答案时,Konrad Rudolph发布了一个采用相同方法的答案,但结果却截然不同。我没有评论他的答案的声誉,所以我将在这里解决它。首先,主要的是他使用的代码每次运行时都会为随机数生成器使用相同的种子。如果更改种子,实际上会得到各种结果。其次,如果你不改变种子,但改变试验次数,你也会得到各种结果。尝试增加或减少一个数量级,看看我的意思。第三,存在一些整数截断或舍入,其中期望值不太准确。它可能不足以产生影响,但它就在那里。

基本上,总的来说,他恰好得到了正确的种子和试验次数,他可能会得到错误的结果。

答案 3 :(得分:2)

可以将随机数生成器视为处理二进制数字流。生成器通过将流切片成块来将流转换为数字。如果std:rand函数使用的是RAND_MAX 32767,那么它在每个切片中使用15位。

当一个人拿到0到32767之间的数字模块时,人们发现5462'0和'1'但只有5461'2','3','4'和'5'。因此结果有偏见。 RAND_MAX值越大,偏差越小,但它是不可避免的。

没有偏差的是[0 ..(2 ^ n)-1]范围内的数字。您可以通过提取3位,将它们转换为0..7范围内的整数并拒绝6和7来生成(理论上)0到5范围内的更好数字。

人们希望比特流中的每个比特都有相等的机会成为'0'或'1',而不管它在流中的位置或其他比特的值。这在实践中非常困难。软件PRNG的许多不同实现在速度和质量之间提供不同的折衷。线性同余生成器(如std::rand)提供最快的速度以获得最低质量。加密发生器为最低速度提供最高质量。