C++中的随机数及其在算法竞赛中的使用

真随机,伪随机

先来讲讲一些基础概念,通常我们使用的随机数算法都是伪随机的,伪随机算法通过某种固定的方法从某个「种子」运算进而得到随机数,这种随机数只满足“统计学伪随机性”,也就是说只是“看上去随机”

如果给定连续几个随机数,那么很有可能就可以推算出之后会使用的随机数,并不安全,所以有时候我们需要更强大的算法来使得给定随机样本的一部分和随机算法,不能有效的演算出随机样本的剩余部分,这样的随机数就称作密码学安全的伪随机数

上面所说的都是伪随机数,只要给定种子,那么就可以得到同样的随机数,真随机数则是随机样本无论如何都不能重现的随机数,通常通过辐射,衰变,噪音等方法生成随机数(但其实严格地说这也不是真正意义上的随机数)

感兴趣可以去 Wikipedia 看看

在算法竞赛中程序的效率是非常重要的,虽然真随机数有各种优点,但是由于需要收集环境的信息,效率通常都比较低,而伪随机数生成器则通常非常高效,我们一般仅获取一个真随机数(有时候只取当前的时间)作为伪随机生成器的种子,再使用伪随机生成器来生成之后的随机数

种子

time(NULL) 是算法竞赛中最常用的种子之一,它包含在头文件 <ctime> 中,它返回一个 time_t 类型(通常就是 long long),其值代表自 1970 年 1 月 1 日以来经过的秒数,但是在同一秒内它返回的值是相同的,有时候时候这会导致很多问题,例如对拍时会生成同样的数据,所以我并不推荐使用它作为种子

std::chrono::system_clock::now().time_since_epoch().count() 或者 std::chrono::steady_clock::now().time_since_epoch().count() 是更好的替代,它们都会返回精度很高的时间(单位为 long long),精度可以达到纳秒,即使连续重复调用多次也会得到不一样的返回值,它们具体的含义和差别可以在网上找到,对于算法竞赛来说用哪个都无所谓,使用时需要头文件 <chrono>

std::random_device 见下文

random_device

std::random_device 是一种随机数生成器类型,如果在 Linux 下调用会通过系统提供的一个系统噪音收集器来获取真随机数,在 Windows 下就会利用很多数据来计算熵,可以近似认为是真随机的(听网上说 GCC 编译器可能会生成同样的随机数,但是我在 Windows 下使用最新版的 GCC 和 旧版的 GCC9 编译都没有出现这个问题)

std::random_device 在头文件 <random> 中,调用时其会返回一个 unsigned int,值域在 \([0,4294967295]\) 也就是 unsigned int 的值域范围

由于是真随机数,效率问题是很明显的(在 Linux 下如果系统没有足够的噪声会造成阻塞,等待系统收集到足够的噪声后才会继续运行),我们通常只用其生成一个随机数作为种子

下面 mt19937 的示例代码中给出了如何以 std::random_device 作为种子

rand

rand() 函数包含在头文件 <cstdlib> 中,调用后会随机(伪随机)返回一个值域在 \([0,\text{RAND\_MAX}]\) (保证均匀分布)的 int 变量

其中 \(\tt{RAND\_MAX}\) 定义在头文件 <cstdlib> 中,一般为 0x7fff,即 \(2^{15}-1=32767\)

在使用前需要先通过 srand() 函数设定随机数种子

下面给出一段示例

#include <cstdlib>
#include <ctime>
#include <iostream>
#include <chrono>
int main()
{
	/* 默认种子为 1 */
	for ( int i = 1; i <= 5; ++i )
		std::cout << rand() << ' ';     /* 41 18467 6334 26500 19169 */
	std::cout << std::endl;

	/* 以当前时间作为种子 */
	srand( std::chrono::system_clock::now().time_since_epoch().count() );
	for ( int i = 1; i <= 5; ++i )
		std::cout << rand() << ' ';     /* 一种可能的输出:19259 8553 3100 1024 13649 */
	std::cout << std::endl;

	/* 设置种子为 1 */
	srand( 1 );
	for ( int i = 1; i <= 5; ++i )
		std::cout << rand() << ' ';     /* 41 18467 6334 26500 19169 */
	std::cout << std::endl;
    
    return 0;
}

由于该函数使用的是线性同余方程生成随机数,所以循环节并不长,在 \(2^{15}\) 以内,同时由于其过小的值域,很多时候并不方便,不过下面还是给出一个简单的实现

int Rand( int x, int y )	/* 获取一个在 [x,y] 范围中的随机数 */
{
	return rand() % (y - x + 1) + x;
}

random_suffle

random_suffle() 函数包含在头文件 <algorithm> 中,传入两个参数分别代表容器的头尾迭代器(也可以传入第三个参数作为随机数生成器,但是对于生成的随机数下标有严格的要求),调用后会将容器内的元素随机打乱,即从所有全排列中等概率选择一个

内部通过 rand() 实现,在使用前需要先通过 srand() 函数设定随机数种子,如果想传入随机数生成器非常麻烦,所以在 C++14 中已经被不推荐使用,在 C++17 中已经被完全弃用,推荐使用 C++11 引入的 shuffle() 函数替代

下面还是给出一段示例

#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
	srand( time( NULL ) );
	std::vector<int>a( { 1, 2, 3, 4, 5 } );
	random_shuffle( a.begin(), a.end() );
	for ( auto x : a ) /* 一种可能的输出:4 1 5 2 3 */
		std::cout << x << ' ';
	return 0;
}

mt19937

这个奇怪的名字来源于这个生成随机数的算法 Mersenne twister(梅森旋转算法),至于其中的 \(19937\) 则是代表它生成的随机数的周期可以到达 \(2^{19937}-1\) 的惊人数量级,相比起 rand() 确实可以避免很多问题,而且 mt19937 几乎是最快的伪随机数生成器,效率非常高

有趣的是与很多人的直觉不同,这个算法其实是由两位日本人发明的,其中使用到了梅森质数,所以这个算法才有了这个名字

std::mt19937 是一个随机数生成器类型,在头文件 <random> 中,调用时其会返回一个 unsigned int,值域在 \([0,4294967295]\) 也就是 unsigned int 的值域范围

如果需要更大的值域范围可以使用 std::mt19937_64,唯一的区别就是返回值变成了一个 unsigned long long,值域也就是 unsigned long long 的值域

定义时注意需要传入一个数作为种子(这里以 std::random_device 为例),下面给出一段示例

#include <iostream>
#include <random>
int main()
{
/*
 * std::random_device rd;
 * std::mt19937 Rnd(rd());
 */
	std::mt19937 Rnd( std::random_device {} () ); /* 这样写等价于上面注释掉的写法,种子为 random_device */
	std::cout << Rnd() << std::endl;
	return 0;
}

shuffle

std::shuffle() 自 C++11 被引入,用于替代 random_shuffle(),同样在 <algorithm> 头文件中,与 random_shuffle() 不同的是它必须传入三个参数,前两个一样是容器的头尾迭代器,最后一个参数是随机数生成器

随机数生成器可以是 std::random_device,可以是 std::mt19937,也可以是 std::default_random_engine(同样 C++11 引入,用于替代 rand() 的随机数生成器,但是性能不及 mt19937,所以还是推荐 mt19937

直接给示例代码

#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
int main()
{
	std::mt19937 Rnd( std::random_device {} () );
	std::vector<int> a( { 1, 2, 3, 4, 5 } );
	std::shuffle( a.begin(), a.end(), Rnd );
	for ( auto x : a ) /* 一种可能的输出:3 2 5 1 4 */
		std::cout << x << ' ';
	return(0);
}

各种 distribution

以下函数均包含于头文件 <random> 中,且均在 std 标准库中,调用时才传入随机数生成器

随机数生成器可以是 std::random_device,可以是 std::mt19937,也可以是 std::default_random_engine

都大同小异就不分开详细讲了

uniform_int_distribution<T>(L,R)

生成一个 \([L,R]\) 值域范围中的随机整数,保证均匀分布,返回值类型为 T,不填写则默认为 int

uniform_real_distribution<T>(L,R)

生成一个 \([L,R)\) 值域范围中的随机实数,保证均匀分布,返回值类型为 T,不填写则默认为 double

normal_distribution<T>(x,y)

生成均值为 \(x\),标准差为 \(y\) 的实数,满足正态分布,返回值类型为 T,不填写则默认为 double

bernoulli_distribution(x)

返回一个 bool,有 \(x\) 的概率为 true


下面是示例代码

#include <iostream>
#include <random>
int main()
{
	std::mt19937 Rnd( std::random_device {} () );

	std::uniform_int_distribution<> dist1( 0, 10 );
	std::cout << dist1( Rnd ) << '\n';      /* 输出 [0,10] 中的一个随机整数 */

	std::uniform_real_distribution<> dist2( -1, 1 );
	std::cout << dist2( Rnd ) << '\n';      /* 输出 [-1,1] 中的一个随机实数 */

	std::normal_distribution<> dist3( 5, 2 );
	for ( int i = 1; i <= 10; ++i )         /* 输出均值为5,标准差为2的10个实数 */
		std::cout << dist3( Rnd ) << '\n';

	std::bernoulli_distribution dist4( 0.2 );
	std::cout << dist4( Rnd ) << '\n';      /* 输出一个数,有0.2的概率是1 */

	return 0;
}

总结

上面这些随机数用法也并没有全部包含所有 C++ 的随机数方法,在 <random> 头文件中还有更多函数,感兴趣可以了解一下

上面所使用的所有头文件都包含在 <bits/stdc++.h> 中,可以只包含这一个头文件就够了


博客园传送门

知乎传送门

posted @ 2022-11-25 14:36  人形魔芋  阅读(770)  评论(1编辑  收藏  举报