Treap、跳跃表和随机快速排序等需要用到随机数,我们要有一种方法来生成它。不过,真正的随机性在计算机中是稀缺的,如果想要在实际应用中使用生成的随机数,太慢是不行的。一般来说,产生伪随机数 pseudorandom number
就足够了。伪随机数是指看上去像是随机的数。随机数有很多的统计性质,而伪随机数满足其中的大部分。然而,生成伪随机数也不是那么容易的。
以抛一枚硬币为例,随机生成 0
正面或者 1
负面。一种做法是借用系统时钟,将时间记作整数,一个从某个起始时刻开始计数的秒数,用其最低的二进制位。如果需要的是随机数序列,这种方法不够理想—— 1 s 1s 1s 是一个长的时间段,程序运行过程中时钟可能没有变化。即使时间用微秒 u s us us 计量,所生成的数的序列也远不是随机的。
我们真正需要的是随机数的序列 random number sequence
。这些数应该独立出现——如果一枚硬币抛出后是正面,那么下次抛出时出现正面还是反面应该是等可能的。
在介绍随机数的生成方法之前,先了解一下模运算。我们常常用 %
取模运算符进行简单的取余数操作,但这不代表我们对模运算了解得有多么深刻。
如果 A − B A-B A−B 可以被 N N N 整除,即 A A A 与 B B B 模 N N N 同余 congruent
,记作 A ≡ B ( m o d N ) A\equiv B(mod\ N) A≡B(mod N) 。直观的看法是:无论 A A A 或者 B B B 被 N N N 除,所得到的余数都是相同的。例子有:
81 ≡ 61 ≡ 1 ( m o d 10 ) 81 \equiv 61\equiv 1(mod\ 10) 81≡61≡1(mod 10)
模运算有着很好的性质,它和等号一样,若 A ≡ B ( m o d N ) A \equiv B(mod\ N) A≡B(mod N) ,则 A + C ≡ B + C ( m o d N ) A+C\equiv B +C(mod\ N) A+C≡B+C(mod N) 以及 A D ≡ B D ( m o d N ) AD \equiv BD(mod\ N) AD≡BD(mod N) 。还有很多定理适用于模运算,有些需要数论的知识,这里不多涉及。
产生随机数最简单的方法是线性同余生成器,更准确的说是乘同余生成器,它于1951年被Lehmer首先提出(1951年,emmm)。数 x 1 , x 2 , … x_1,x_2,\dots x1,x2,… 的生成满足:
x i + 1 = A x i m o d M x_{i+1} = Ax_i\ \ mod\ \ M xi+1=Axi mod M
为了开始序列的生成,我们必须给出 x 0 x_0 x0 的某个值,也就是我们平常说的随机数种子 seed
。如果给出的 x 0 = 0 x_0=0 x0=0 ,这个序列就远不是随机的。
但是如果 A , M A, M A,M 被正确选择,那么任何其他的 x 0 ( 1 ≤ x 0 < M ) x_0(1\le x_0 \lt M) x0(1≤x0<M) 都是同等有效的。更加有意思的是,如果 M M M 是素数,那么 x i x_i xi 就绝不会是 0 0 0 。同样举一个例子, M = 11 , A = 7 , x 0 = 1 M = 11, A = 7, x_0 = 1 M=11,A=7,x0=1 ,生成的序列如下:
7 , 5 , 2 , 3 , 10 , 4 , 6 , 9 , 8 , 1 , 7 , 5 , 2 , 3 , 10 ⋯ 7, 5, 2, 3, 10, 4, 6, 9, 8, 1, 7, 5, 2, 3, 10\cdots 7,5,2,3,10,4,6,9,8,1,7,5,2,3,10⋯
我们还可以注意到一个特点:在 M − 1 = 10 M - 1= 10 M−1=10 个数后,序列将会出现重复,或者说,这个序列的周期为 M − 1 M - 1 M−1 。而我们,必须使得生成的随机数序列的周期尽可能大(鸽巢定理)。我们还知道的是:如果 M M M 是素数,那么总是存在某些 A A A 的值,能够得到全周期 M − 1 M - 1 M−1,但是也有些 A A A 的值得不到这样的周期。如 M = 11 , A = 5 , x 0 = 1 M = 11, A = 5, x_0 = 1 M=11,A=5,x0=1 ,其产生的序列有一个短周期 5 5 5 :
5 , 3 , 4 , 9 , 1 , 5 , 3 , 4 , … 5, 3, 4, 9, 1, 5, 3, 4, \dots 5,3,4,9,1,5,3,4,…
综上所述,如果我们选择一个很大的素数作为 M M M ,同时选择一个合适的 A A A 值,我们将得到一个周期很长的序列——Lehmer的建议是使用素数 M = 2 32 − 1 = 2147483647 M = 2^{32}- 1 = 2147483647 M=232−1=2147483647 ,针对它, A = 48271 A = 48271 A=48271 是给出全周期生成器的许多值的一个。
这个程序实现起来很简单,类变量 state
保存 x x x 序列的当前值。不过调试随机数程序时,最好是置 x 0 = 1 x_0 = 1 x0=1 ,这使得总是出现相同的随机序列。使用随机数程序时,可以用系统时钟的值,也可以让用户输入一个值作为 seed
。
另外,返回一个位于 ( 0 , 1 ) (0,1) (0,1) 的随机实数也是常见的;可以转换类变量为 d o u b l e double double 然后除以 M M M 得到。从而,任意闭区间 [ α , β ] [\alpha, \beta] [α,β] 内的随机数都可以通过规范化得到。
代码如下,不过有错误:
static const int A = 48271;
static const int M = 2147483647;
class Random {
private:
int state;
public:
//禁止类构造函数的隐式类型转换
//使用传入的初值构造state
explicit Random(int initialValue = 1) {
if (initialValue < 0)
initialValue += M;
//保证state为正数
state = initialValue;
if (state <= 0)
state = 1;
}
//返回一个伪随机整数,改变内部state
//有bug
int randomInt() {
return state = (A * state) % M;
}
//返回一个伪随机实数,(0,1)之间,同时改变内部state
double random0_1() {
return (double) randomInt() / M;
}
};
问题在于:乘法可能溢出。这将影响计算的结果和伪随机性。不过经过改造,我们可以让这个过程中所有的计算在32位上进行而不溢出。计算 M / A M / A M/A 的商和余数,并分别定义为 Q , R Q, R Q,R 。此时, Q = 44488 , R = 3399 , R < Q Q = 44488, R = 3399, R < Q Q=44488,R=3399,R<Q :
x i + 1 = A x i m o d M = A x i − M ⌊ A x i M ⌋ = A x i − M ⌊ x i Q ⌋ + M ⌊ x i Q ⌋ − M ⌊ A x i M ⌋ = A x i − M ⌊ x i Q ⌋ + M ( ⌊ x i Q ⌋ − ⌊ A x i M ⌋ ) x i = Q ⌊ x i Q ⌋ + x i m o d Q \begin{aligned} x_{i + 1} &= Ax_i\ mod\ M \\ &= Ax_i - M \lfloor {Ax_i \over M }\rfloor \\ &= Ax_i - M \lfloor {x_i \over Q }\rfloor + M \lfloor {x_i \over Q }\rfloor - M \lfloor {Ax_i \over M }\rfloor \\ &= Ax_i - M \lfloor {x_i \over Q }\rfloor + M \Big( \lfloor {x_i \over Q }\rfloor - \lfloor {Ax_i \over M }\rfloor \Big)\\ x_i &= Q \lfloor {x_i \over Q}\rfloor + x_i\ mod\ Q \end{aligned} xi+1xi=Axi mod M=Axi−M⌊MAxi⌋=Axi−M⌊Qxi⌋+M⌊Qxi⌋−M⌊MAxi⌋=Axi−M⌊Qxi⌋+M(⌊Qxi⌋−⌊MAxi⌋)=Q⌊Qxi⌋+xi mod Q
所以有:
x i + 1 = A ( Q ⌊ x i Q ⌋ + x i m o d Q ) − M ⌊ x i Q ⌋ + M ( ⌊ x i Q ⌋ − ⌊ A x i M ⌋ ) = ( A Q − M ) ⌊ x i Q ⌋ + A ( x i m o d Q ) + M ( ⌊ x i Q ⌋ − ⌊ A x i M ⌋ ) \begin{aligned} x_{i + 1} &= A\Big( Q \lfloor {x_i \over Q} \rfloor + x_i\ mod\ Q\Big) - M \lfloor {x_i \over Q }\rfloor + M \Big( \lfloor {x_i \over Q }\rfloor - \lfloor {Ax_i \over M }\rfloor \Big)\\ &= (AQ - M) \lfloor {x_i \over Q }\rfloor + A(x_i\ mod\ Q) + M \Big( \lfloor {x_i \over Q }\rfloor - \lfloor {Ax_i \over M }\rfloor \Big)\\ \end{aligned} xi+1=A(Q⌊Qxi⌋+xi mod Q)−M⌊Qxi⌋+M(⌊Qxi⌋−⌊MAxi⌋)=(AQ−M)⌊Qxi⌋+A(xi mod Q)+M(⌊Qxi⌋−⌊MAxi⌋)
由于 M = A Q + R M = AQ + R M=AQ+R ,所以 A Q − M = − R AQ - M = -R AQ−M=−R ,得到:
x i + 1 = A ( x i m o d Q ) − R ⌊ x i Q ⌋ + M ( ⌊ x i Q ⌋ − ⌊ A x i M ⌋ ) = A ( x i m o d Q ) − R ⌊ x i Q ⌋ + M δ ( x i ) δ ( x i ) = 0 o r 1 \begin{aligned} x_{i + 1} &= A(x_i\ mod\ Q) -R\lfloor {x_i \over Q }\rfloor + M \Big( \lfloor {x_i \over Q }\rfloor - \lfloor {Ax_i \over M }\rfloor \Big)\\ &= A(x_i\ mod\ Q) -R\lfloor {x_i \over Q }\rfloor + M \delta(x_i) \\ \delta(x_i) &= 0\ or\ 1 \end{aligned} xi+1δ(xi)=A(xi mod Q)−R⌊Qxi⌋+M(⌊Qxi⌋−⌊MAxi⌋)=A(xi mod Q)−R⌊Qxi⌋+Mδ(xi)=0 or 1
验证表明,因为 R < Q R < Q R<Q ,所以所有余项均可计算没有溢出;另外,当 A ( x i m o d Q ) − R ⌊ x i Q ⌋ A(x_i\ mod\ Q) -R\lfloor {x_i \over Q }\rfloor A(xi mod Q)−R⌊Qxi⌋ 小于 0 0 0 时( ⌊ x i Q ⌋ , ⌊ A x i M ⌋ \lfloor {x_i \over Q }\rfloor,\lfloor {Ax_i \over M }\rfloor ⌊Qxi⌋,⌊MAxi⌋ 都是整数,它们的差不是 0 0 0 就是 1 1 1 ), δ ( x i ) = 1 \delta(x_i) = 1 δ(xi)=1 。所以,我们可以通过简单的测试确定 δ ( x i ) \delta(x_i) δ(xi) 的值。
不溢出的最终程序如下:
static const int A = 48271;
static const int M = 2147483647;
static const int Q = M / A;
static const int R = M % A;
class Random {
private:
int state;
public:
//禁止类构造函数的隐式类型转换
//使用传入的初值构造state
explicit Random(int initialValue = 1) {
if (initialValue < 0)
initialValue += M;
//保证state为正数
state = initialValue;
if (state <= 0)
state = 1;
}
//返回一个伪随机整数,改变内部state
//有bug
int randomInt() {
//return state = (A * state) % M;
int tempState = A * (state % Q) - R * (state / Q);
if (tempState >= 0)
state = tempState;
else
state = tempState + M;
return state;
}
//返回一个(0,1)之间的伪随机实数,同时改变内部state
double random0_1() {
return (double)randomInt() / M;
}
//返回一个闭区间[low, high]的随机整数,改变内部状态
int randomInt(int low, int high) {
double partitionSize = (double)M / (high - low + 1);
return (int)(randomInt() / partitionSize) + low;
}
};
更复杂一点的,我们可以添加一个常数,得到下面的递推公式:
x i + 1 = ( A x i + C ) m o d M \displaystyle x_{i+1} = (Ax_i + C) \bmod M xi+1=(Axi+C)modM
其中:
由这四个整数定义的混合同余序列得到最大周期长度 M − 1 M - 1 M−1 的条件如下:
更多随机数生成算法看这篇文章:戳这里。
实际应用中,我们不需要手写随机数生成器,C++给我们提供了一个相当强大的随机数算法——mt19937
。
mt19937
是C++11中加入的新特性,作为一种随机数算法,用法与 rand()
函数类似,但是速度快、周期长——它的命名就来自周期长度 2 19937 − 1 2^{19937}-1 219937−1 。写在程序中,这个函数的随机范围大概在 [INT_MIN,INT_MAX]
之间。它的用法非常简单:
#include
#include
std::mt19937 rnd(time(0));
int main() {
printf("%lld\n", rnd());
return 0;
}