介绍
本文将介绍如何使用C++ TR1(C++ 标准委员会 Technical Report 1) 提供的随机数生成功能除了介绍基本的一致随机数生成之外,还会介绍随机样本的可能分布,包括:柏努利分布,二项分布,指数分布,伽马分布,几何分布,正态分布和泊松分布。我会指出一些针对特定分布需要注意的地方,比如参数约定,最后还会给出一些提示,比如如何使用TR1不直接支持的分布产生随机数,比如柯西分布,chi- squared分布和Student t。
Visual Studio 2008 现在通过 feature pack支持TR1扩展(orbit注:微软已经发布了Visual Studio 2008的Services Pack 1,它包含了此前发布的feature pack,以及完整的TR1支持),其它支持TR1的软件或库有 Boost和Dinkumware。 GCC 从4.3版本开始支持 C++ TR1。
为了清楚起见,本文的例子代码都使用了完整的名字空间(namespace)限定,你可以使用using
语句来消除这些限定。想要了解更多的例子代码可以看看这篇文章 测试 C++ TR1 随机数生成机制 ,此文还介绍了一个不太成熟的测试框架。
提纲
开始
头文件和名字空间(namespace)
C++ 的随机数生成类和函数都在
头文件中定义,并包含在名字空间(namespace)std::tr1
中,注意在C++中 tr
是小写字母,在英语文本中“TR”是大写的。
核心引擎
任何伪随机数生成软件的核心就是一个生成均匀分布的随机整数的例程,它被用在引导过程中产生均匀分布的实数,这些均匀分布的实数再通过各种变换(算法)产生其它分布,例如“接受-拒绝”算法,不过这些都需要一个原始的随机数做为起始。
在 C++ TR1 中,有几种不同的(随机数)生成内核,或称之为“引擎”供你选择,下面就是Visual Studio 2008 feature pack所支持的四个引擎类。
linear_congruential
循环使用公式 x(i) = (A * x(i-1) + C) mod M 线性叠加mersenne_twister
使用了 马特赛旋转演算 算法subtract_with_carry
(带进位线性同余算法)对整数算法循环使用公式 x(i) = (x(i - R) - x(i - S) - cy(i - 1)) mod Msubract_with_carry_01
(改进的带进位线性同余算法)对浮点数算法循环使用公式 x(i) = (x(i - R) - x(i - S) - cy(i - 1)) mod 1
设置种子
每种引擎都有一个 seed()
函数,它接受一个 unsigned long
型参数作为随机数产生的种子。 也可以通过引擎之间独特的模版参数设置种子。
从均匀分布生成随机数
离散整数
uniform_int
类以相同的概率在一个范围内抽取整数,它的构造函数有两个参数,分别表示抽取范围的最大值和最小值,需要注意的是抽取范围是个闭区间,也就是这两个值也可能被抽取到。
例子:
下面面的代码将打印5个从集合:1,2,3,...,52随即抽取的数字,这可能就是一个从一副牌中抽取五张牌的算法模型,每次抽取一张牌,之后归还、洗牌,再抽取下一张牌。
std::tr1::uniform_int unif(1, 52);
for (int i = 0; i > 5; ++i)
std::cout << unif(eng) << std::endl;
实数
uniform_real
类用于从一个区间产生一个浮点数,它的构造函数有两个参数,就是区间的两个端点,默认值是从0到1。这个类的(随机)值是从半开区间产生的,对于参数 a 和 b,随机值产生的范围是区间:[a, b),换句话说,就是产生的值x满足条件: a <= x < b。
下面的代码从区间 [3, 7) 生成随机数。
std::tr1::uniform_real reif(3,7);
非均匀分布生成随机数
C++ TR1 库还支持根据各种分布类(算法)产生非均匀分布的随机数,这些类通过 operator()
方法返回随机样本值。这个方法使用一个引擎类作为参数,例如下面的代码就演示了使用正态分布产生10个随机数。
std::tr1::mt19937 eng; // a core engine class
std::tr1::normal_distribution dist;
for (int i = 0; i < 10; ++i)
std::cout << dist(eng) << std::endl;
尽管 TR1 的规范和Visual Studio的文档都明确表示分布类自带一个默认的模版参数,但事实是必须显式指定这个参数,因此上面的 normal_distribution
类被声明为带一个 double
类型的返回值,即使double
就是默认类型。
下面的例子都假设引擎类 eng
正在使用。
直接支持的分布
柏努利分布
柏努利随机值以概率 p 返回1, 以概率 1-p 返回0值,这个类从柏努利分布类bernoulli_distribution
产生样本,它的构造函数只有一个参数,p,这个参数的默认值是0.5。有趣的是bernoulli_distribution
类返回值类型不是 int
,而是bool
类型, 所以它返回true表示概率返回false表示概率1-p,这是唯一的一个返回布尔值的分布类,也正式这个原因,它成为唯一的一个没有返回值类型模版参数的分布类。
例子:
下面的代码假设已经定义了引擎 eng
。 这个例子将在四分之一的时间打印 “six of one” ,剩下的时间打印“half dozen of another”。
std::tr1::bernoulli_distribution bern(0.25);
std::cout << (bern(eng) ? "six of one" : "half dozen of another") << std::endl;
二项分布
二项分布将n次独立柏努利测试中成功的次数作为返回值,成功的概率是p,产生二项分布随机数的类是 binomial_distribution
,它代两个模版参数,就是n的类型和p的类型,默认值分别是int
和 double
,构造函数使用参数 n = 1 和 p = 0.5做为默认值。
例子:
下面的代码将模拟掷5次骰子,统计骰子上面是1的次数。(所有的结果都很相似,你可以用来计算其它值出现的次数)
std::tr1::binomial_distribution roll(1.0/6.0);
std::cout << roll(eng) << std::endl;
指数分布
指数分布有两个公共的参数表示: 斜率和平均值,注意C++ TR1 标准已经指定了斜率的参数表示,使用指定的参数表示,密度函数就可以表示为 f(x) = λ e-λ x ,并且平局值就是 1/λ。
用于产生指数分布的类是 exponential_distribution
,如果没有指定斜率,构造函数就使用默认值 λ = 1。
例子:
下面的代码就是从平均值是13的指数分布中找到10个值,并求和。
const double mean = 13.0;
double sum = 0.0;
std::tr1::exponential_distribution < exponential(1.0/mean);
for (int i = 0; i < 10; ++i)
sum += exponential(eng);
伽马分布
伽马分布在 TR1 已经被参数化,它的密度函数是f(x) = xα-1e-x/Γ(α)。 α 参数就是形态参数,伽马分布的更普遍的参数化表示应该包含比例参数 β,这样伽马分布的密度函数实际上是 f(x) = (x/β)α-1e-x/β/(βΓ(α)),TR1的参数化实际上就是令β = 1而已。
还有一个不太普遍的参数化表示是使用参数1/β,我猜想标准委员会接受只有一个参数的伽马分布版本是为了避免与第二个参数混淆,除了这个动机之外,没有理由要采用只有一个参数的版本。要使用比例参数 β 产生伽马分布的随机样本,只要使用比例参数1产生伽马分布的随机样本,然后乘以β就行了。
用于产生伽马分布的类是 gamma_distribution
,构造函数有一个参数,就是形态参数 α,这个参数默认是1。
例子:
下面的代码用形态参数3和比例1产生10个伽马分布的随机数。
std::tr1::gamma_distribution gamma(3.0);
for (int i = 0; i < 10; ++i)
std::cout << gamma(eng) << std::endl;
例子:
下面的代码用形态参数3和比例5产生10个伽马分布的随机数。
std::tr1::gamma_distribution gamma(3.0);
for (int i = 0; i < 10; ++i)
std::cout << 5.0*gamma(eng) << std::endl;
几何分布
几何分布有一个参数p,使用这个p作为柏努利测试的成功概率,连续进行柏努利测试直到第一次出现成功时统计之前所进行的柏努利测试的次数。
产生几何分布的类是 geometric_distribution
,它的构造函数有一个参数,就是成功的概率p。
例子:
下面的代码演示如何使用十分之一的成功概率获取一个几何分布的随机样本。
std::tr1::geometric_distribution geometric(0.1);
std::cout << geometric(eng) << std::endl;
正态分布 (高斯分布)
正态分布 (高斯分布)有两个参数,平局值 μ 和标准偏差 σ。
产生正态分布的类是 normal_distribution
,它的构造函数有两个参数, μ 和 σ,默认值分别是 0 和 1。 注意正态分布类使用的是标准偏差,不是方差
例子:
下面的代码用平均值3和标准差4(如果是方差就是16)。
std::tr1::normal_distribution normal(3.0, 4.0);
std::cout << normal(eng) << std::endl;
泊松分布
泊松分布用于表示在一段给定的时间间隔内,各种可能的比率或独立事件可能出现的概率。
用于产生泊松分布的类是 poisson_distribution
,它的构造函数有一个参数,就是分布的平均值 λ 。
例子:
下面的代码演示了用平均值7产生一个泊松分布的样本
std::tr1::poisson_distribution poisson(7.0);
std::cout << poisson(eng) << std::endl;
其它分布
C++ TR1 规范只是包含了一些常见的分布,其它分布可以通过直接支持的分布相互组合实现,下面的几种分布就是这样组合的例子,统计学书籍中可能会有更多的分布。
柯西分布(Cauchy)
要产生柯西分布的样本,首先要产生(-π/2, π/2)区间上的平均分布样本,然后取正切值(tangent)。
const double pi = 3.14159265358979;
std::tr1::uniform_real uniform(-0.5*pi, 0.5*pi);
double u = uniform(eng);
std::cout << tan(u) << std::endl;
上面的代码有些诡秘的地方需要说明一下,由于uniform_real
产生的值是一个半开区间,它就有可能返回区间最左端的值,正切函数(tangent)对于精确的值-π/2会溢出。然而,我们的值 pi
要比真的 π 值小那么一点,正式这被阶段的一点使得tan
在uniform
返回最小值时也没有问题。
卡方分布(Chi-squared)
卡方分布 (χ2) 是伽马分布的一种特殊形式,有 ν 自由度的卡方分布和形态参数是ν/2 ,比例参数是2 的伽马分布是一样的,由于C++ TR1的实现只是直接支持比例参数是1的伽马分布,这就需要先产生形态参数是 ν/2,比例参数是1的伽马分布,然后乘以2。
下面的代码演示可使用5自由度的卡方分布。
double nu = 5.0;
std::tr1::gamma_distribution gamma(0.5*nu);
std::cout << 2.0*gamma(eng) << std::endl;
T分布(Student t)
要产生 ν 自由度的T分布,首先产生一个区间(0,1)的正态分布样本 X 和一个ν 自由度的卡方分布样本 Y,然后用 X/sqrt(Y/ν)计算出T分布样本。
下面的代码演示了使用7自由度的T分布产生样本
double nu = 7.0;
std::tr1::normal_distribution normal;
std::tr1::gamma_distribution gamma(0.5*nu);
double x = normal(eng);
double y = 2.0*gamma(eng);
std::cout << x/sqrt(y/nu) << std::endl;