不要使用rand():C++中的随机数生成器指南1
不要使用 rand()
。为什么?直接上代码。下面的代码大约会输出什么值?
#include <cstdlib>
#include <iostream>
using namespace std;
const int ITERATIONS = 1e7;
int main() {
double sum = 0;
for (int i = 0; i < ITERATIONS; i++)
sum += rand() % 1000000;
cout << "Average value: " << sum / ITERATIONS << '\n';
}
应该是 500 , 000 500,000 500,000 左右,对吗?事实证明,这取决于编译器,在 Codeforces 上2,它输出的是 16382 16382 16382,根本不接近预期。自己试一试。
这里发生了什么?
如果你查一下 C++ 文档中的 rand()
,你会发现它返回“一个介于 0
和 RAND_MAX
之间的伪随机整数”。点击 RAND_MAX
,你会看到“这个值取决于实现。保证这个值至少是
32767
32767
32767”。在 Codeforces 上, RAND_MAX
正好是
32767
32767
32767。这实在是太小了!
但这还不止于此,random_shuffle()
也使用了 rand()
。回顾一下,为了随机地打乱大小为
n
n
n 的数组,我们需要生成
n
n
n 个随机位置 。但是如果 rand()
只能达到
32767
32767
32767,那么如果我们在一个元素数量明显多于这个数的数组上调用 random_shuffle()
会发生什么?是时候编写更多的代码了。你认为下面的代码能输出什么?
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
const int N = 3000000;
double average_distance(const vector<int> &permutation) {
double distance_sum = 0;
for (int i = 0; i < N; i++)
distance_sum += abs(permutation[i] - i);
return distance_sum / N;
}
int main() {
vector<int> permutation(N);
for (int i = 0; i < N; i++)
permutation[i] = i;
random_shuffle(permutation.begin(), permutation.end());
cout << average_distance(permutation) << '\n';
}
这将计算出每个值在随机打乱中移动的平均距离。如果你计算一下,你会发现在完全随机的打乱中,答案应该为 N 3 = 1 , 000 , 000 \frac{N}{3}=1,000,000 3N=1,000,000。即使你不想做数学题,你也可以观察到答案介于 N 2 = 1 , 500 , 000 \frac{N}{2}=1,500,000 2N=1,500,000,即到位置 0 0 0 的平均距离和 N 4 = 750 , 000 \frac{N}{4}=750,000 4N=750,000,即到位置 N 2 \frac{N}{2} 2N 的平均距离之间。
好吧,上面的代码再次让人失望;它输出了
64463
64463
64463。自己试一试。换句话说,random_shuffle()
将每个元素平均移动了数组长度的
2
%
2\%
2% 的距离。根据我的测试,Codeforces上 random_shuffle()
的实现与以下内容完全一致。
for (int i = 1; i < N; i++)
swap(permutation[i], permutation[rand() % (i + 1)]);
因此,如果 RAND_MAX
远小于
N
N
N,这种打乱自然会有问题。
不仅仅是 RAND_MAX
太小的问题,rand()
本身有更多的质量问题。它通常用一个相对简单的线性同余发生器(LCG)实现。在 Codeforces 编译器上,它像是这样:
static long holdrand = 1L;
void srand(unsigned int seed) {
holdrand = (long) seed;
}
int rand() {
return (((holdrand = holdrand * 214013L + 2531011L) >> 16) & 0x7fff);
}
特别是,LCG 在低位的可预测性极强。第 k k k 位(第 0 0 0 位为最低位)的周期最多为 2 k + 1 2^{k+1} 2k+1(即序列重复所需的时间),所以最低位的周期只有 2 2 2,第二个最低位的周期是 4 4 4,等等。这就是为什么上面的函数抛弃了最低的 16 16 16 位,结果输出最多为 32767 32767 32767。
解决方法是什么?
别担心,从 C++11 开始,C++ 中有更好的随机数生成器可用。你唯一需要记住的是使用包含在 <random>
头文件中的 mt19937
。这是一个基于素数
2
19937
−
1
2^{19937}-1
219937−1 的 Mersenne Twister(梅森旋转算法),这个数字也恰好是它的周期。它是一个比 rand()
质量高得多的随机数发生器(RNG),而且速度更快(在 Codeforces 中,从 mt19937
生成和添加
1
0
8
10^8
108 个数字需要
389
389
389 毫秒,而 rand()
需要
1170
1170
1170 毫秒)。它还能在
0
0
0 和
2
32
−
1
=
4294967295
2^{32}-1=4294967295
232−1=4294967295 之间产生完整的
32
32
32 位无符号输出,而不是最大
32767
32767
32767。
可以调用 shuffle()
来取代 random_shuffle()
,并将你的 mt19937
作为第三个参数传入,shuffle 算法将使用你提供的发生器进行打乱。
C++11 还为你提供了一些有趣的分布。 uniform_int_distribution
为你提供了完全均匀的数字,没有 mod 的偏差——即 rand() % 10000
更有可能给你一个
0
0
0 到
999
999
999 的数字,而不是
9000
9000
9000 到
9999
9999
9999 的数字,因为
32767
32767
32767 不是
10000
10000
10000 的倍数。还有许多其他有趣的分布,包括正态分布和指数分布。
为了给你一个更具体的概念,这里有一些使用上面提到的几个工具的代码。请注意,该代码使用高精度时钟对随机数发生器进行播种。这对于避免专门针对你的代码的 hack3 很重要,因为使用一个固定的种子意味着任何人都可以确定你的 RNG 会输出什么。更多细节,请看 How randomized solutions can be hacked, and how to make your solution unhackable(随机化方法如何被 hack,以及如何使您的方法不被 hack)。
最后一件事:如果你想要
64
64
64 位的随机数,就用 mt19937_64
代替。
#include <algorithm>
#include <chrono>
#include <iostream>
#include <random>
#include <vector>
using namespace std;
const int N = 3000000;
double average_distance(const vector<int> &permutation) {
double distance_sum = 0;
for (int i = 0; i < N; i++)
distance_sum += abs(permutation[i] - i);
return distance_sum / N;
}
int main() {
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
vector<int> permutation(N);
for (int i = 0; i < N; i++)
permutation[i] = i;
shuffle(permutation.begin(), permutation.end(), rng);
cout << average_distance(permutation) << '\n';
for (int i = 0; i < N; i++)
permutation[i] = i;
for (int i = 1; i < N; i++)
swap(permutation[i], permutation[uniform_int_distribution<int>(0, i)(rng)]);
cout << average_distance(permutation) << '\n';
}
两次打乱的平均距离都几乎是 1 0 6 10^6 106,就像我们最初预期的那样。
其他参考资料
这篇文章的灵感部分来自 Stephan T. Lavavej 的演讲 “rand() Considered Harmful”:
https://learn.microsoft.com/zh-cn/events/goingnative-2013/rand-considered-harmful
如果你想要更快、更高质量的随机数生成器,可以看看 Sebastiano Vigna 的这个网站。
译自 neal 的 Don’t use rand(): a guide to random number generators in C++,在 DeepL 机器翻译基础上修改,参考 百度百科 、百度翻译 和 OI Wiki ↩︎
译者注:指使用 Codeforces 的 Custom Invocation 测试 ↩︎
译者注:指 Codeforces 中的 hack 功能而并非所谓的黑客攻击 ↩︎