随机数与随机函数

在一些算法中可能运用到一些与随机相关的内容,对随机相关内容有一定了解是必须的。随机数是由随机数生成器生成的。

真随机数与伪随机数

真随机数是用物理现象产生的,是完全随机的。例如抛硬币,抛骰子等都能产生真随机数。真随机数具有不可预测性

真随机数依赖于物理随机数生成器,技术要求较高。

伪随机数是利用算法模拟产生的,结果是确定的,这样的随机数我们认为其是伪随机的。

可以看出真伪随机数差别就在于是否可预测。

在一些重要的场合,例如加密等,涉及到随机的部分通常使用真随机数以保证安全。

C++ 中的真随机数发生器

random_device 是基于硬件的的非确定性均匀分布随机数发生器,该类定义于 C++11,需要 random 头文件,定义于 std 命名空间中。

熵池耗尽 前可以高速生成随机数,熵池耗尽后性能急剧下降。所以通常使用 random_device 生成 mt19937 等伪随机数发生器的种子。

如果不支持真随机数生成,标准允许使用伪随机数引擎实现。

引用 cppreference 中的实例:

c++
#include <iostream>
#include <map>
#include <random>
#include <string>
 
int main()
{
    std::random_device rd;
    std::map<int, int> hist;
    std::uniform_int_distribution<int> dist(0, 9);
 
    for (int n = 0; n != 20000; ++n)
        ++hist[dist(rd)]; // 注意:仅用于演示:一旦熵池耗尽,
                          // 许多 random_device 实现的性能就急剧下滑
                          // 对于实践使用,random_device 通常仅用于
                          // 播种类似 mt19937 的伪随机数生成器
 
    for (auto [x, y] : hist)
        std::cout << x << " : " << std::string(y / 100, '*') << '\n';
}

可能的输出:

0 : ********************
1 : *******************
2 : ********************
3 : ********************
4 : ********************
5 : *******************
6 : ********************
7 : ********************
8 : *******************
9 : ********************

C++ 中的伪随机数发生器

rand

该函数会返回一个位于 [0,RAND_MAX] 范围的随机数,可以通过取模限定返回值范围。其中 RAND_MAX 是系统预定义的一个宏,不能更改。在 linux 环境下为 23112^{31}-1,而 windows 系统下为 2151=327672^{15}-1=32767。windows 环境下这个数是较小的一个数,部分情况下不能支持需求。

rand 比较慢,并且随机效果通常不太好。所以一般不推荐使用。

mt19937

该类定义于 C++11,包含于 random 头文件。随机数范围同 unsigned int 取值范围。随机数质量较高。类似的 mt19937_64 随机数范围扩大到 unsigned long long

使用实例:

c++
#include <ctime>
#include <iostream>
#include <random>

using namespace std;

int main() {
  mt19937 myrand(time(0)); // 定义时括号内填写种子,也可以不填
  cout << myrand() << endl;
  return 0;
}

随机数种子

随机数种子决定了整个随机序列,在同一编译器同一种子的情况下,随机生成的序列是同一的。可以使用 time(0) 作为随机数种子。time 函数会返回系统时间。但是注意这个返回值一秒变化一次,如果在对拍等场合会比较低效,因为同一秒调用若干次,得到的数据是相同的。所以可以使用 random_device 进行播种。但是该类又依赖于熵池。建议使用 std::chrono::steady_clock::now().time_since_epoch().count() 函数来得到一个以毫秒计量的时间,以该时间作为随机数种子,注意使用该函数需要包含 chrono 头文件。

随机数分布

有时候,我们希望随机数在一个范围内均匀随机,但是随机数发生器的返回值并不一定满足我们的需求,有几种手段来将随机数限定在希望的范围内。

取模

将返回值进行取模,得到范围内的随机数。

c++
mt19937 Rad();
int rad(int l, int r) {
  return Rad() % (r - l + 1) + l;
}

这样有缺陷:得到的结果不一定均匀。因为 Rad 每一个返回值对 r-l+1 取模的结果并不是均匀分布的。

使用 C++ 标准提供的随机数分布

std::uniform_int_distribution 类可以返回一个范围内均匀随机的整数分布。

cppreference 上提供的实例,此程序模拟一个六面体骰子:

c++
#include <iostream>
#include <random>
 
int main()
{
    std::random_device rd;  // 随机数引擎的种子源
    std::mt19937 gen(rd()); // 以 rd() 播种的 mersenne_twister_engine
    std::uniform_int_distribution<> distrib(1, 6);
    // 上方的 <> 中填写的是返回值的类型,缺省为 int
    // 可以 std::uniform_int_distribution<long long> distrib(L, R);
    // 得到一个返回值为 L, R 的一个均匀分布。
 
    // 用 distrib 变换 gen 所生成的随机 unsigned int 为 [1, 6] 中的 int
    for (int n = 0; n != 10; ++n)
        std::cout << distrib(gen) << ' ';
    std::cout << '\n';
}

实际使用可以这样写来避免繁琐的声明:

c++
mt19937 Rad();
int rad(int l, int r) {
  return uniform_int_distribution<int>(l, r)(Rad);
}

类似的,随机数分布还有很多种,可能还会用到 uniform_real_distribution,该类返回一个实数类型的均匀分布,区间左闭右开。

快速沃尔什变换
Kirchhoff 矩阵-树定理