【算法学习】随机化算法 随机数生成器和mt19937

文章目录

  • 1. 伪随机数
  • 2. 模运算
  • 3. 乘同余法随机数生成器
    • (1) 原理
    • (2) 程序实现
  • 4. 混合同余法
  • 5. mt19937


1. 伪随机数

Treap跳跃表随机快速排序等需要用到随机数,我们要有一种方法来生成它。不过,真正的随机性在计算机中是稀缺的,如果想要在实际应用中使用生成的随机数,太慢是不行的。一般来说,产生伪随机数 pseudorandom number 就足够了。伪随机数是指看上去像是随机的数。随机数有很多的统计性质,而伪随机数满足其中的大部分。然而,生成伪随机数也不是那么容易的。

以抛一枚硬币为例,随机生成 0 正面或者 1 负面。一种做法是借用系统时钟,将时间记作整数,一个从某个起始时刻开始计数的秒数,用其最低的二进制位。如果需要的是随机数序列,这种方法不够理想—— 1 s 1s 1s 是一个长的时间段,程序运行过程中时钟可能没有变化。即使时间用微秒 u s us us 计量,所生成的数的序列也远不是随机的。

我们真正需要的是随机数的序列 random number sequence 。这些数应该独立出现——如果一枚硬币抛出后是正面,那么下次抛出时出现正面还是反面应该是等可能的。


2. 模运算

在介绍随机数的生成方法之前,先了解一下模运算。我们常常用 % 取模运算符进行简单的取余数操作,但这不代表我们对模运算了解得有多么深刻。

如果 A − B A-B AB 可以被 N N N 整除,即 A A A B B B N N N 同余 congruent ,记作 A ≡ B ( m o d   N ) A\equiv B(mod\ N) AB(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) 81611(mod 10)

模运算有着很好的性质,它和等号一样,若 A ≡ B ( m o d   N ) A \equiv B(mod\ N) AB(mod N) ,则 A + C ≡ B + C ( m o d   N ) A+C\equiv B +C(mod\ N) A+CB+C(mod N) 以及 A D ≡ B D ( m o d   N ) AD \equiv BD(mod\ N) ADBD(mod N) 。还有很多定理适用于模运算,有些需要数论的知识,这里不多涉及。


3. 乘同余法随机数生成器

(1) 原理

产生随机数最简单的方法是线性同余生成器,更准确的说是乘同余生成器,它于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(1x0<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 M1=10 个数后,序列将会出现重复,或者说,这个序列的周期为 M − 1 M - 1 M1 。而我们,必须使得生成的随机数序列的周期尽可能大(鸽巢定理)。我们还知道的是:如果 M M M 是素数,那么总是存在某些 A A A 的值,能够得到全周期 M − 1 M - 1 M1,但是也有些 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=2321=2147483647 ,针对它, A = 48271 A = 48271 A=48271 是给出全周期生成器的许多值的一个。

(2) 程序实现

这个程序实现起来很简单,类变量 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=AxiMMAxi=AxiMQxi+MQxiMMAxi=AxiMQxi+M(QxiMAxi)=QQxi+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(QQxi+xi mod Q)MQxi+M(QxiMAxi)=(AQM)Qxi+A(xi mod Q)+M(QxiMAxi)
由于 M = A Q + R M = AQ + R M=AQ+R ,所以 A Q − M = − R AQ - M = -R AQM=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)RQxi+M(QxiMAxi)=A(xi mod Q)RQxi+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)RQxi 小于 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;
	}
};

4. 混合同余法

更复杂一点的,我们可以添加一个常数,得到下面的递推公式:
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 M M 是模数, 0 < M \displaystyle 0 < M 0<M ;(为了达到预期的随机效果,一般希望这个值稍稍大一点,且最好是素数)
  • A A A 乘数, 1 ≤ A < M \displaystyle {1 \le A \lt M} 1A<M ;(实用起见最好取大于等于 2 2 2 的值)
  • C C C 增量, 0 < C < M \displaystyle {0\lt C\lt M} 0<C<M ;(当 C = 0 C=0 C=0 时,随机数生成过程比不等于零时要稍微快些。它的限制可能缩短这个序列的周期长度,但也有可能得到一个相当长的周期。当 C = 0 C=0 C=0 时被称为乘同余法, C ≠ 0 C\ne 0 C=0 称为混合同余法。为了一般性,建议选择采用混合同余法)
  • x 0 x_0 x0 开始值, 0 ≤ x 0 < M \displaystyle 0 \le x_0 \lt M 0x0<M

由这四个整数定义的混合同余序列得到最大周期长度 M − 1 M - 1 M1 的条件如下:

  1. C , M C,M C,M 互为素数;
  2. 对于整除 M M M 的每个素数 P P P B = A − 1 B = A - 1 B=A1 P P P 的倍数;
  3. 如果 M M M 4 4 4 的倍数,那么 B B B 也是 4 4 4 的倍数。

更多随机数生成算法看这篇文章:戳这里。


5. mt19937

实际应用中,我们不需要手写随机数生成器,C++给我们提供了一个相当强大的随机数算法——mt19937

mt19937 是C++11中加入的新特性,作为一种随机数算法,用法与 rand() 函数类似,但是速度快、周期长——它的命名就来自周期长度 2 19937 − 1 2^{19937}-1 2199371 。写在程序中,这个函数的随机范围大概在 [INT_MIN,INT_MAX] 之间。它的用法非常简单:

#include
#include
std::mt19937 rnd(time(0));

int main() {
	printf("%lld\n", rnd());
	return 0;
}

你可能感兴趣的:(======算法======,随机化,C++)