C 随机库

C语言提供的 <cstdlib> 库,包含 两个函数和一个常量

定义

  • int rand();:返回 [​0​ … RAND_MAX] 的随机数。
  • void srand( unsigned seed );:以值 seed 播种 随机数生成器,用于后续的rand()
  • RAND_MAXrand() 生成的最大可能值。依赖于编译器实现,保证至少为 32767

基本用法

基于以下四点

  • 以相同的 seed播种,产生的随机数序列相同。
  • 若未调用 srand(),则 rand() 表现如同它以 srand(1) 播种。
  • 通常来说,在程序开始或者任何到 rand() 的调用前,应该只播种一次随机数生成器。
  • 标准实践是以当前时间作为种子。
1
2
3
4
5
6
7
8
9
10
11
#include <cstdlib>
#include <iostream>
#include <ctime>

int main()
{
std::srand(std::time(0)); // 以当前时间为随机数生成器的种子
int random_variable = std::rand() % 100; // 生成0-99随机数
std::cout << "Random value on [0 " << RAND_MAX << "]: "
<< random_variable << '\n';
}

模偏差

在上述生成0到99的随机数算法中,如果只关心一个随机数,那没问题。但如果要生成随机数序列,就会存在模偏差问题。即 rand()%100 并不能以相等的概率产生介于0和99之间的数字!
因为 rand()%n 的结果并不一定能等概率映射到0~n-1上,除非 RAND_MAX%n == n - 1

均匀分布

为了解决模偏差问题,首先定义一个新的 RAND_MAX_N =RAND_MAX - RAND_MAX % n,则 RAND_MAX_N%n == n - 1,这样就能等概率映射了;其次为了得到 0RAND_MAX_N 的随机数,需要引入拒绝采样。简单来说就是,一直生成,拒绝超出范围的,直到符合条件。

为什么不直接用拒绝采样生成0到n-1的随机数?
因为执行次数的数学期望太高了。每次只有 n/RAND_MAX的概率在规定范围内获得一个值,平均需要调用RAND_MAX/n 次 rand()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <cstdlib>
#include <iostream>
#include <ctime>

int main()
{
std::srand(std::time(nullptr)); // 以当前时间为随机生成器的种子
int random_variable = std::rand();
std::cout << "Random value on [0 " << RAND_MAX << "]: "
<< random_variable << '\n';

// 扔 6 面色子 20 次
for (int n = 0; n != 20; ++n) {
int x = 1 + rand() % 6; // 注意: 1 + rand() % 6 有偏差!
std::cout << x << ' ';
}
std::cout << std::endl;
// 扔 6 面色子 20 次
for (int n = 0; n != 20; ++n) {
int x;
do { // 拒绝采样
x = rand();
} while (x >= (RAND_MAX - RAND_MAX % 6));
x = x % 6 + 1;
std::cout << x << ' ';
}
return 0;
}

C++ 标准库

stl 包含了随机数库<random>,其中定义了一组随机数引擎类模板和随机数分布类模板。它通过随机数引擎产生均匀的随机数序列,随机数分布使用随机数引擎生成服从特定概率分布的随机数。

对于随机数生成,有多个分布类模板的原因是:我们在给定场景下生成的序列需要依靠数据的特性。医院前来就诊的病人的模式可能和到商店的顾客的模式有很大不同,因此需要运用不同的分布。而且,商店顾客的模式可能会有所不同,取决于商店的种类及其位置,因此对于不同商店的顾客的到达模型,可能也需要运用不同的分布。

之所以有多种随机数引擎和生成器,是因为没有一种算法可以生成适合所有情况的随机数。相比其他算法,有一些算法可以生成更长的无重复序列;一些算法要求的计算开销更小。当知道想模型化的数据的特性时,就能够决定使用哪种分布和哪种随机序列生成能力。

随机数引擎

标准库定义了一组随机数引擎类模板适配器模板,同时使用不同的算法预定义了一组随机数引擎类模板的实例作为随机数生成器。此外还提供了基于硬件源的非确定随机数生成器。

随机数引擎类

随机数引擎类是一个定义了生成随机位序列的无符号整数序列机制的类模板。STL定义了3个代表随机数引擎的类模板。这些是可以定制的。

随机数引擎适配器

随机数引擎适配器是一个类模板,它通过修改另一个随机数引擎生成的序列来生成随机数序列。<random> 中提供了三种不同功能的适配器。

预定义随机数生成器

随机数生成器是随机数引擎类模板的一个预定义的实例。每一个生成器都会将一套具体的模板参数应用到随机数引擎的类模板上,因此它是一个类型别名
STL提供了几个预定义的随机数生成器,为了生成随机数,它们实现了一些著名的算法。因为是对三种随机数引擎类模板的实例化,所以也分成三组:同组之间使用相同的算法,只是参数类型不同。

  • LCG线性同余法随机数生成器
    • minstd_rand0
    • minstd_rand
  • 梅森旋转随机数生成器
    • mt19937
    • mt19937_64
  • 带进位减法随机数生成器
    • ranlux24_base
    • ranlux48_base

这些预定义生成器定义了算法的最佳实践,避免了参数的选择,可以直接选择引擎,设定分布即可。
此外,根据编译器设置的默认引擎类型(上述三种之一),还可以使用类型别名default_random_engine 来简单快捷地生成随机数。

非确定随机数生成器

std::random_device 定义的函数对象可以生成用来作为种子的随机的无符号整数值。这个类使用非确定性数据源,它通常是由操作系统提供的,通过硬件生成真正的不确定随机数(如果硬件不支持,标准也允许使用伪随机生成方法实现此函数)。
非确定性数据源可以是连续敲打键盘的时间区间,或者鼠标点击的区间,或者当前的时钟时间,或者一些物理特性。

随机数分布

随机数分布处理随机数生成器的输出序列,使得产生的随机数序列服从指定的概率密度函数分布。STL提供了为各种不同的分布定义函数对象的类模板。
下面只给出部分代表类模板,更多请参考 STL random

  • 均匀分布
    • uniform_int_distribution,产生在一个范围上均匀分布的整数值
    • uniform_real_distribution,产生在一个范围上均匀分布的实数值
  • 伯努利分布
    • bernoulli_distribution,产生伯努利分布上的 bool
    • binomial_distribution,产生二项分布上的整数值
  • 泊松分布
    • poisson_distribution,产生泊松分布上的整数值
    • exponential_distribution,产生指数分布上的实数值
  • 正态分布
    • normal_distribution,产生标准正态(高斯)分布上的实数值
    • lognormal_distribution,产生对数正态分布上的实数值
  • 采样分布
    • discrete_distribution,产生离散分布上的随机整数

快速使用

生成服从指定分布的随机数一般分为四步

  1. 生成随机数种子
  2. 为随机数生成器播种
  3. 定义随机数分布
  4. 按照指定的分布生成随机数

以下是一个快速使用 <random> 的例子,在具体使用上仍有许多可探究之处,留待下次一定。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <random>
#include <iomanip>

using std::cout;
using std::endl;
using std::setw;
using std::setfill;
using std::left;

constexpr int MIN = 0;
constexpr int MAX = 100;

constexpr int RAND_NUMS_TO_GENERATE = 10;

int main()
{
std::random_device rd; // 非确定源随机数生成器,硬件生成真随机数
std::default_random_engine eng(rd()); // 为默认随机数引擎播种
std::uniform_int_distribution<int> uniform_dist(MIN, MAX); // 定义区间上的均匀分布

for (int n = 0; n < RAND_NUMS_TO_GENERATE; ++n) {
cout << left << setfill(' ') << setw(8) << uniform_dist(eng) << " ";
}
cout << endl;

int seed = uniform_dist(eng); // 使用均匀分布产生随机数作为种子
std::mt19937 mt(seed); // 为mt引擎播种
std::normal_distribution<> normal_dist{50, 2};

for (int n = 0; n < RAND_NUMS_TO_GENERATE; ++n) {
cout << left << setfill(' ') << setw(8) << normal_dist(mt) << " ";
}
cout << endl;

return 0;
}

输出如下图所示。
image-20220727002529112

参考资料

https://www.delftstack.com/howto/cpp/generate-random-number-in-range-cpp/
https://zh.cppreference.com/w/cpp/numeric/random
http://c.biancheng.net/view/635.html