从范围生成随机整数


157

我需要一个函数,该函数将在给定范围内(包括边界值)生成一个随机整数。我没有不合理的质量/随机性要求,我有四个要求:

  • 我需要它快。我的项目需要生成数百万(有时甚至数千万)的随机数,而我目前的生成器功能已被证明是瓶颈。
  • 我需要它合理地统一(使用rand()完全可以)。
  • 最小-最大范围可以是<0,1>到<-32727,32727>的任何值。
  • 它必须是可播种的。

我目前有以下C ++代码:

output = min + (rand() * (int)(max - min) / RAND_MAX)

问题是,它不是真正统一的-仅当rand()= RAND_MAX时才返回max(对于Visual C ++,它是1/32727)。对于像<-1,1>这样的小范围,这是一个主要问题,在该范围中几乎永远不会返回最后一个值。

所以我拿起笔和纸,想出了以下公式(该公式基于(int)(n + 0.5)整数舍入技巧):

在此处输入图片说明

但是它仍然不能给我均匀的分布。重复运行10000个样本后,值-1、0的比例为37:50:13.1。

你能建议更好的配方吗?(甚至整个伪随机数生成器函数)



3
@比尔·马格里夫:是的。它有同样的问题。一个简化的版本是:如何将10块糖果平均分配给3个孩子(不破坏任何糖果)?答案是,您不能-您必须给每个孩子三个,而不要给任何孩子第十个。
杰里·科芬

5
您看过Boost.Random吗?
Fred Nurk 2011年

3
查看安德鲁·科尼格(Andrew Koenig)的文章“一个几乎永远无法正确解决的简单问题”:drdobbs.com/blog/archives/2010/11/a_simple_proble.html
Gene Bushuyev 2011年

1
@Gene Bushuyev:我和安德鲁都已经在这个问题上思考了很长时间了。请参阅:groups.google.com/group/comp.lang.c++ / browse_frm
杰里·科芬

Answers:


105

一种快速的解决方案,比您的解决方案要好一些,但是仍然不能提供统一的分布式解决方案

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

除非范围的大小是2的幂,否则此方法都会产生有偏差的非均匀分布数,而与的质量无关rand()。有关此方法质量的全面测试,请阅读此内容


2
谢谢,从快速测试中看来,这对我来说已经足够了--1、0、1的分布接近33:33:33。
马捷Zábský

3
它总是返回最大值。我在这里想念什么吗?:|
rohan-patel

15
rand()在C ++中应该被认为是有害的,有更好的方法来获得均匀分布且实际上是随机的东西。
Mgetz 2013年

1
它真的会在100%的时间内返回正确的数字吗?我在这里找到了其他一些stackoverflow答案,这些答案使用递归来“正确地进行”:stackoverflow.com/a/6852396/623622
Czarek Tomczak 2014年

2
因为这是一个非常令人讨厌的答案(对于期望而言),对于许多新读者来说,这似乎是可靠的信息来源,所以我认为提及此解决方案的质量和潜在危险非常重要,因此我进行了编辑。
–plasmcel

296

最简单(因此也最好)的C ++(使用2011标准)的答案是

#include <random>

std::random_device rd;     // only used once to initialise (seed) engine
std::mt19937 rng(rd());    // random-number engine used (Mersenne-Twister in this case)
std::uniform_int_distribution<int> uni(min,max); // guaranteed unbiased

auto random_integer = uni(rng);

无需重新发明轮子。无需担心偏见。无需担心将时间用作随机种子。


1
如今,这应该是答案。有关更多功能的伪随机数生成参考
alextoind

8
我同意“最简单”(也是最惯用的),而不是“最好”。不幸的是,该标准不提供任何保证random_device,在某些情况下可能会被完全破坏。此外,mt19937虽然很好的通用选择,但并不是高质量发生器中最快的(请参见此比较),因此可能不是OP的理想选择。
Alberto M

1
@AlbertoM不幸的是,您所指的比较没有提供足够的细节,并且无法再现,因此令人怀疑(此外,它是从2015年开始,而我的回答可以追溯到2013年)。确实有更好的方法可以解决(并且希望将来minstd会是这样的方法),但这就是进展。至于-的糟糕实现,random_device那太可怕了,应该被认为是一个错误(如果允许的话,也可能是C ++标准的错误)。
Walter

1
我完全同意你的看法; 我实际上并不想批评您的解决方案本身,只是想警告随便的读者,尽管有C ++ 11的承诺,但关于此事的明确答案尚待编写。我将在2015年发布该主题的概述,作为相关问题的答案。
艾伯托M

1
那是“最简单的”吗?您能否详细说明为什么rand()不是简单得多的一种选择,对于非关键用途(例如生成随机枢轴索引),这是否重要?另外,我担心构建random_device/ mt19937/ uniform_int_distribution在紧凑循环/内联函数?我是否应该更喜欢传递它们?
bluenote10 '16

60

如果您的编译器支持C ++ 0x,并且可以选择使用它,那么新的标准<random>标头可能会满足您的需求。它具有高质量uniform_int_distribution,可以接受最小和最大范围(包括您所需要的范围),并且您可以在各种随机数生成器中进行选择以插入该分布。

这是生成int在[-57,365]中均匀分布的一百万个随机数的代码。我已经使用了新的std <chrono>工具为它计时,因为您提到性能是您的主要关注点。

#include <iostream>
#include <random>
#include <chrono>

int main()
{
    typedef std::chrono::high_resolution_clock Clock;
    typedef std::chrono::duration<double> sec;
    Clock::time_point t0 = Clock::now();
    const int N = 10000000;
    typedef std::minstd_rand G;
    G g;
    typedef std::uniform_int_distribution<> D;
    D d(-57, 365);
    int c = 0;
    for (int i = 0; i < N; ++i) 
        c += d(g);
    Clock::time_point t1 = Clock::now();
    std::cout << N/sec(t1-t0).count() << " random numbers per second.\n";
    return c;
}

对我来说(2.8 GHz Intel Core i5)可以打印出:

每秒2.10268e + 07随机数。

您可以通过将int传递给它的构造函数来为生成器设置种子:

    G g(seed);

如果您以后发现int不满足您的发行范围,则可以通过将此类更改uniform_int_distribution(例如long long)来解决:

    typedef std::uniform_int_distribution<long long> D;

如果您以后发现minstd_rand生成器的质量不够高,也可以很容易地将其替换掉。例如:

    typedef std::mt19937 G;  // Now using mersenne_twister_engine

对随机数生成器具有单独的控制,并且随机分布可以完全解放。

我还计算了(未显示)此分布的前四个“矩”(使用minstd_rand),并将它们与理论值进行了比较,以试图量化分布的质量:

min = -57
max = 365
mean = 154.131
x_mean = 154
var = 14931.9
x_var = 14910.7
skew = -0.00197375
x_skew = 0
kurtosis = -1.20129
x_kurtosis = -1.20001

x_前缀表示“预期”)


3
该答案可以使用简短的摘要代码段,该摘要代码段仅显示生成某个范围内的随机整数所需的代码。
arekolek 2015年

分布的最小值和最大值从不改变,这使问题变得更加容易。如果您必须d在每次迭代中都创建具有不同界限的怎么办?它会减慢环路多少?
–quant_dev

15

让我们将问题分为两个部分:

  • 生成一个n介于0到(max-min)之间的随机数。
  • 在该号码上加上分钟

第一部分显然是最难的。假设rand()的返回值是完全统一的。使用取模将给第一个(RAND_MAX + 1) % (max-min+1)数字增加偏差。因此,如果我们可以神奇地更改RAND_MAXRAND_MAX - (RAND_MAX + 1) % (max-min+1),就不会再有任何偏差。

事实证明,如果我们愿意允许伪不确定性进入算法的运行时间,则可以使用这种直觉。每当rand()返回一个太大的数字时,我们只要求另一个随机数,直到得到一个足够小的数字。

现在,运行时间以几何形状分布,具有期望值1/p,其中p指的是第一次尝试获得足够小的数字的概率。由于RAND_MAX - (RAND_MAX + 1) % (max-min+1)始终小于(RAND_MAX + 1) / 2,我们知道p > 1/2,因此对于任何范围,预期的迭代次数将始终小于两次。使用这种技术,应该可以在不到一秒钟的时间内在标准CPU上生成数千万个随机数。

编辑:

尽管以上在技术上是正确的,但DSimon的答案在实践中可能更有用。您不应该自己实现这些东西。我已经看到了很多拒绝采样的实现,通常很难看到它是否正确。


为了完整起见:这是拒绝采样
etarion'2

3
有趣的事实:Joel Spolsky曾经提到此问题的一个版本,作为StackOverflow擅长回答的一个示例。我通过对现场涉及拒绝抽样的答案看着那个时间和 一个是不正确的。
约根·福

13

如何在梅森难题?Boost实现非常易于使用,并且在许多实际应用中都经过了良好的测试。我已经在一些学术项目中使用过它,例如人工智能和进化算法。

在他们的示例中,他们制作了一个简单的功能来滚动六边形模具:

#include <boost/random/mersenne_twister.hpp>
#include <boost/random/uniform_int.hpp>
#include <boost/random/variate_generator.hpp>

boost::mt19937 gen;

int roll_die() {
    boost::uniform_int<> dist(1, 6);
    boost::variate_generator<boost::mt19937&, boost::uniform_int<> > die(gen, dist);
    return die();
}

哦,这是该生成器的更多附加功能,以防万一您不相信应该在劣等的产品上使用它rand()

Mersenne Twister是松本诚和西村隆司发明的“随机数”生成器。他们的网站包含该算法的众多实现。

本质上,梅森扭曲器是一个非常大的线性反馈移位寄存器。该算法对19,937位种子进行操作,该种子存储在由624个元素组成的32位无符号整数数组中。值2 ^ 19937-1是梅森素数;操纵种子的技术基于较旧的“扭曲”算法-因此被称为“ Mersenne Twister”。

Mersenne Twister的一个吸引人的方面是它使用二进制运算(而不是费时的乘法)来生成数字。该算法的周期也很长,粒度也很好。对于非加密应用程序,它既快速又有效。


1
梅森捻线机是一个很好的生成器,但是不管底层生成器本身如何,他要解决的问题仍然存在。
杰里·科芬

我不想仅将Boost用于随机生成器,因为(因为我的项目是一个库),这意味着要对该项目引入另一个依赖关系。将来无论如何我可能都会被迫使用它,因此可以切换到该生成器。
马捷Zábský

1
@Jerry Coffin哪个问题?我之所以提供它,是因为它满足了他的所有要求:快速,统一(使用boost::uniform_int分布),您可以将最小最大范围转换为所需的任何值,并且可以播种。
Aphex

@mzabsky我可能不会阻止我,当我不得不将我的项目交付给我的教授提交时,我只包含了我正在使用的相关的boost头文件;您不必将整个40mb boost库与您的代码打包在一起。当然,在您的情况下,由于其他原因,例如版权,这可能不可行...
Aphex

@Aphex 我的项目不是真正的科学模拟器,也不是真正需要统一分发的东西。我使用旧的生成器1.5年没有任何问题,当我第一次需要它生成很小范围内的数字(在这种情况下为3)时,我才注意到有偏差的分布。速度仍然是考虑采用增强解决方案的理由。我将查看其许可证,看看是否可以将一些所需的文件添加到我的项目中-我现在喜欢“签出-> F5->准备使用”。
马捷Zábský

11
int RandU(int nMin, int nMax)
{
    return nMin + (int)((double)rand() / (RAND_MAX+1) * (nMax-nMin+1));
}

这是32768个整数到(nMax-nMin + 1)个整数的映射。如果(nMax-nMin + 1)很小(根据您的要求),则映射将非常好。但是请注意,如果(nMax-nMin + 1)大,则映射将不起作用(例如,您无法将32768值映射到30000值的概率相等)。如果需要这样的范围,则应使用32位或64位随机源,而不是15位的rand(),或忽略超出范围的rand()结果。


尽管它不受欢迎,但这也是我在非科学项目中使用的方法。易于理解(不需要数学学位)并且表现出色(无需使用它来分析任何代码)。:)在大范围的情况下,我想我们可以将两个rand()值串在一起,并获得一个30位的值来使用(假设RAND_MAX = 0x7fff,即15个随机位)
efotinis 2011年

进行更改RAND_MAX(double) RAND_MAX避免整数溢出警告。
Alex

4

这是一个无偏见的版本,它在中生成数字[low, high]

int r;
do {
  r = rand();
} while (r < ((unsigned int)(RAND_MAX) + 1) % (high + 1 - low));
return r % (high + 1 - low) + low;

如果您的范围相当小,则没有理由在do循环中缓存比较的右侧。


海事组织,提出的解决方案都没有真正改善。他的基于循环的解决方案有效,但效率可能很低,尤其是对于OP讨论的小范围。他的统一偏差解决方案实际上根本不会产生统一偏差。至多它伪装缺乏统一性。
杰里·科芬

@Jerry:请检查新版本。
耶利米·威尔考克

我不确定是否可以正常工作。可能,但是正确性似乎并不明显,至少在我看来。
杰里·科芬

@Jerry:这是我的理由:假设范围是[0, h)为了简单起见。调用rand()RAND_MAX + 1可能的返回值;将它们rand() % h折叠(RAND_MAX + 1) / h到每个h输出值,除了将(RAND_MAX + 1) / h + 1它们映射到小于(RAND_MAX + 1) % h(由于通过h输出的最后部分循环)之外。因此,我们删除了(RAND_MAX + 1) % h可能的输出以获得无偏分布。
Jeremiah Willcock

3

我建议使用Boost.Random库,它非常详细且文档齐全,可让您显式指定所需的分布,并且在非加密方案中,实际上可以胜过典型的C库rand实现。


1

假设min和max是int值,[和]表示包括此值,(和)表示不包括此值,使用c ++ rand()使用上述方法获得正确的值

参考:对于()[]定义,请访问:

https://zh.wikipedia.org/wiki/间隔(数学)

对于rand和srand函数或RAND_MAX定义,请访问:

http://en.cppreference.com/w/cpp/numeric/random/rand

[最小,最大]

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

(最小,最大)

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

[最小,最大)

int randNum = rand() % (max - min) + min

(最小,最大)

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

0

在该线程拒绝采样中已经讨论过,但是我想提出一个基于上述事实的优化方法,该方法rand() % 2^something不会引入任何偏差。

该算法非常简单:

  • 计算大于间隔长度2的最小幂
  • 在“新”间隔中随机分配一个数字
  • 如果该数字小于原始间隔的长度,则返回该数字
    • 否则拒绝

这是我的示例代码:

int randInInterval(int min, int max) {
    int intervalLen = max - min + 1;
    //now calculate the smallest power of 2 that is >= than `intervalLen`
    int ceilingPowerOf2 = pow(2, ceil(log2(intervalLen)));

    int randomNumber = rand() % ceilingPowerOf2; //this is "as uniform as rand()"

    if (randomNumber < intervalLen)
        return min + randomNumber;      //ok!
    return randInInterval(min, max);    //reject sample and try again
} 

这尤其适用于较小的时间间隔,因为2的乘方将“接近”实际时间间隔的长度,因此未命中的次数将减少。

PS
显然,避免递归会更有效(无需一遍又一遍地计算对数上限。)但是我认为此示例更易读。


0

请注意,在大多数建议中,您从rand()函数获得的初始随机值(通常从0到RAND_MAX)被简单地浪费了。您只能在其中创建一个随机数,而有一个声音程序可以为您提供更多。

假设您想要整数随机数的[min,max]区域。我们从[0,max-min]开始

取底b = max-min + 1

从代表从b的rand()获得的数字开始。

这样,您便有了floor(log(b,RAND_MAX)),因为除可能的最后一位外,基数b中的每个数字都表示范围为[0,max-min]的随机数。

当然,对于每个随机数r + min,最终移至[min,max]很简单。

int n = NUM_DIGIT-1;
while(n >= 0)
{
    r[n] = res % b;
    res -= r[n];
    res /= b;
    n--;
}

如果NUM_DIGIT是您可以提取的以b为底的位数,即

NUM_DIGIT = floor(log(b,RAND_MAX))

那么以上内容是从提供b <RAND_MAX的一个RAND_MAX随机数中提取0到b-1的NUM_DIGIT随机数的简单实现。


-1

公式非常简单,因此请尝试使用此表达式,

 int num = (int) rand() % (max - min) + min;  
 //Where rand() returns a random number between 0.0 and 1.0

2
整个问题是使用C / C ++的rand来返回运行时指定范围内的整数。如该线程所示,如果要避免破坏它们的统计属性或性能,将随机整数从[0,RAND_MAX]映射到[MIN,MAX]并不是很简单。如果在[0,1]范围内有双精度数,则映射很容易。
马捷Zábský

2
您的答案是错误的,应改用模数:int num = (int) rand() % (max - min) + min;
Jaime Ivan Cervantes

-2

如果我没有记错的话,下面的表达应该是公正的:

std::floor( ( max - min + 1.0 ) * rand() ) + min;

我在这里假设rand()为您提供一个介于0.0和1.0之间的随机值,不包括1.0,并且max和min是整数,且min <max。


std::floor返回double,我们在这里需要一个整数值。我只是int选择使用而不是std::floor
musiphil
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.