从游戏到密码学,从抽样到预测,生成随机数对于各种应用程序而言都是很必要的。但是,“随机数”一词的表述实际上并不正确,因为通过数学公式生成的数字是确定的,不会产生真正的随机数,但数字看起来是随机的,因此叫作伪随机数。真正的随机性只能通过基于物理过程的硬件设备来实现,即便如此,也可能会受到挑战,因为我们甚至可能认为宇宙实际上也是确定的。现代C++支持通过包含数字生成器和分布的伪随机数库生成伪随机数。理论上来说,它也可以产生真正的随机数,但在实践中那些可能只是伪随机数。
在本节中,我们将讨论标准库对生成伪随机数的支持,其中理解随机数和伪随机数之间的差异是关键。真正的随机数是指不能通过随机过程偶然预测的数字,它是借助硬件随机数生成器生成的。而伪随机数是在算法的帮助下生成的数字,这些算法生成的序列具有与真正的随机数近似的特性。
此外,熟悉各种统计分布更佳,但是你必须知道什么是均匀分布,因为库中的所有引擎都生成均匀分布的数字。在不深入讨论任何细节的情况下,我们只会提到均匀分布是一种概率分布,它与等可能发生的事件(在一定范围内)有关。
要在应用程序中生成伪随机数,应该执行以下步骤:
1)包含头文件<random>:
2)使用std::random_device生成器来初始化伪随机数引擎:
3)使用其中一个生成数字的引擎,并使用随机种子对其进行初始化:
4)使用一个可用的分布将引擎的输出转换为所需的统计分布:
5)生成伪随机数:
伪随机数库包含两种类型的组件:
❍ 引擎,它们是随机数的生成器,既可以产生服从均匀分布的伪随机数,也可以产生实际随机数(如果有的话)。
❍ 将引擎的输出转换为统计分布的分布。
所有引擎(除了random_device)产生的整数都服从均匀分布,并且所有引擎都可以实现以下方法:
❍ min():这是一个静态方法,它返回生成器可以生成的最小值。
❍ max():这是一个静态方法,它返回生成器可以生成的最大值。
❍ seed():使用起始值初始化算法(random_device除外,它无法被种子初始化)。
❍ operator():生成一个均匀分布在min()和max()之间的新数字。
❍ discard():生成并丢弃给定数量的伪随机数。
系统支持以下引擎:
❍ linear_congruential_engine:这是一个线性同余生成器。它使用以下公式生成数字:
x ( i )=( Ax ( i -1)+ C ) mod M
❍ mersenne_twister_engine:这是一个梅森旋转(Mersenne twister)生成器,它在 W ( N -1) R 位上保留一个值。每次需要生成一个数字时,它提取 W 位,所有的位都被使用后,它通过移动和混合位来扭曲大的值,以便有一组新的位可来提取。
❍ subtract_with_carry_engine:这是一个基于以下公式实现进位减法算法的生成器:
x ( i )=( x ( i-R ) -x ( i-S ) -cy ( i -1)) mod M
在上式中, cy 定义为:
此外,该库还提供了引擎适配器,引擎适配器也是一种引擎,它包裹着其他引擎并且基于基础引擎的输出生成数字。引擎适配器实现了之前提到的基础引擎提供的相同方法。目前有以下引擎适配器可用:
❍ discard_block_engine:使基础引擎生成的块(包含 P 个数字)中保留 R 个数字,并丢弃其余的数字的生成器。
❍ independent_bits_engine:生成与基础引擎不同位数的数字的生成器。
❍ shuffle_order_engine:该生成器保存一个由基础引擎生成的 K 个数字组成的shuffle表,并从该表中返回数字(用基础引擎生成的数字替换它们)。
伪随机数生成器应该根据应用程序的具体要求来选择。线性余同引擎速度中等,但对其内部状态的存储要求非常小;进位减法引擎速度非常快,包括那些没有高级算术指令集处理器机器的引擎,但是它需要更大的存储空间来存储其内部状态,而且生成的数字序列具有较少的理想特性;梅森旋转引擎速度最慢、存储时间最长,但能生成最长的非重复伪随机数序列。
所有这些引擎和引擎适配器都会产生伪随机数。然而,该库提供了另一个名为random_device的引擎,该引擎本应该生成不确定的数字,但事实上没有实际的限制,因为随机熵的物理来源可能不可用。因此,random_device的实现实际上可以基于伪随机数引擎。random_device类不能像其他引擎那样被种子初始化,它有一个名为entropy()的附加方法,该方法返回随机设备熵,对于确定性生成器,它是0,对于非确定性生成器,它是非零。
然而,这并不是一种确定该设备实际上是确定性的还是非确定性的可靠的方法。例如,GNU libstdc++库和LLVM libc++库都实现了一个非确定性设备,但熵返回0;另外,对于vc++库和boost.random库,熵的返回值分别为32和10。
所有这些生成器产生的整数都服从均匀分布,然而,均匀分布只是大多数应用程序中需要的随机数服从的众多统计分布中的一种。为了能够利用其他分布生成数字(整数或实数),该库提供了几个分布类,它们根据引擎实现的统计分布转换引擎的输出。目前可用的分布如表2.1所示。
表2.1
正如前面提到的,库提供的每一个引擎都有优点和缺点。梅森旋转引擎虽然是最慢的,且内部状态最大,但如果进行适当的初始化,可以产生最长的非重复数字序列。在下面的示例中,我们将使用std::mt19937(内部状态有19937位的32位梅森旋转引擎)。
生成随机数最简单的方法如下:
在这个示例中,mtgen是梅森旋转引擎std::mt19937。要生成数字,只需要使用调用操作符来迭代内部状态并返回下一个伪随机数。然而,这段代码是有缺陷的,因为引擎无法用种子初始化。所以,它总是产生相同的数字序列,在大多数情况下这可能不是你想要的。
初始化引擎有不同的方法,C语言的random库的一种常见方法是使用当前时间作为种子。而在现代C++中,它应该是这样的:
在这个示例中,seed表示从时钟的纪元到当前时刻的tick数,用作初始化引擎的种子。这种方法的问题是,seed的值实际上是确定的,而且在某些类型的应用程序中,它可能很容易受到攻击。一种更可靠的方法是用实际随机数作为生成器的种子。
std::random_device类是一个应该返回真正的随机数的引擎,虽然std::random_device类的实现确实基于伪随机数生成器,但是它理应返回真正的随机数:
所有引擎产生的数字都服从均匀分布。为了将结果转换为另一个统计分布,我们必须使用分布类。为了显示生成的数字是如何依据所选定的分布来分布的,我们将使用以下函数。这个函数生成了指定数量的伪随机数,并且计算它们在map中的重复次数,然后用map的值生成柱状图,以展示每个数字出现的频率:
以下代码使用std::mt19937引擎生成随机数,这些随机数在[1,6]内均匀分布,这基本上是通过掷骰子能够得到的结果:
程序的输出如图2.1所示。
图2.1 [1, 6]内的均匀分布
在本节的最后一个示例中,我们将分布改为均值为5、标准差为2的正态分布。这种分布生成实数,因此为了使用前面的generate_and_print()函数,数字必须四舍五入为整数:
上述代码的输出结果如图2.2所示。
图2.2 均值为5、标准差为2的正态分布
如图2.2所示,分布已经从均匀分布变为正态分布(均值为5)。
❍ 阅读2.4节,以了解如何正确初始化随机数引擎。