不要使用rand():C++中的随机数生成器指南

本文讨论了C++中rand()函数的问题,包括其返回值范围小、随机性不足等,并介绍了C++11引入的更好的随机数生成器。建议使用<random>库中的Mersenne Twister算法,以获得更高质量且速度更快的随机数。同时,文章还提到了种子的重要性以及不同分布的使用,以确保更均匀的随机分布。
摘要由CSDN通过智能技术生成

不要使用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(),你会发现它返回“一个介于 0RAND_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 2199371Mersenne 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 2321=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 的这个网站


  1. 译自 nealDon’t use rand(): a guide to random number generators in C++,在 DeepL 机器翻译基础上修改,参考 百度百科百度翻译OI Wiki ↩︎

  2. 译者注:指使用 Codeforces 的 Custom Invocation 测试 ↩︎

  3. 译者注:指 Codeforces 中的 hack 功能而并非所谓的黑客攻击 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值