你知道 C++ 中的 rand() 函数是怎么实现的吗?你知道怎么在一维 / 二维 / 三维空间中等可能地随机取点吗?
随机数是一系列看似无规律、无法预测的数字或值的序列,其产生过程具有一定程度的不确定性。在计算机中,真正的绝对随机数是很难获得的,因为计算机本质上是基于确定性的操作。因此,计算机常常使用称为 “伪随机数生成器”(Pseudo-Random Number Generator, PRNG)的算法来生成序列,这些序列表现出一定的随机性特征,但实际上是通过初始值(随机数种子)推演出来的。在一些需要高度随机性和安全性的场合,则会使用硬件随机数生成器,从而确保系统和数据的保密性、完整性和可靠性。
一. 随机数的本质
计算机生成随机数的方法有很多种,常用的有以下几种:
-
使用
rand()
函数:rand()
函数是 C 语言标准库中的随机数生成函数,本质上是一个伪随机数生成器 (PRNG),使用线性同余法实现:/** 线性同余法实现伪随机数生成器 N[j+1] = (A * N[j] + B) % M gcc 中 A = 1103515245, B = 12345, M = 2^31 */ unsigned int next = -1; // 随机数种子 int rand(void){ if (next == -1) { // 未指定种子时使用系统时间作为随机数种子 next = (unsigned int)time(NULL); } next = (next*1103515245+12345)%2147483648; return (unsigned int)(next); } void srand(unsigned int seed){ // 修改随机数种子 next = seed; }
其中 next 为随机数种子,在程序的一次执行中不断迭代,后一次调用使用前一次生成的随机数作为种子代入线性同余法公式计算得到随机数。
srand()
函数可以指定初始的随机数种子,或在程序执行过程中修改随机数种子。当初始随机数种子未被指定时,使用系统时间作为随机数种子。由于rand()
函数是一个伪随机数生成器,因此只要设定的初始随机数种子相同,生成的随机数序列也是一样的; -
硬件随机数生成器 (HRNG):利用物理过程中的不确定性来生成真随机数,HRNG依赖于物理过程,理论上是不可预测的。硬件随机数生成器可以利用多种物理现象来获取随机性:
- 热噪声:热噪声是由电子运动引起的微小涨落,使用电阻器中的热噪声来生成随机数;
- 放射性衰变:放射性同位素的衰变是一种随机性过程,也可以用来生成真随机数;
- 光学噪声:使用光学噪声源,如光二极管的光强涨落,来产生随机性;
- 时钟抖动:基于时钟抖动的生成器利用计算机内部时钟的微小抖动来获取随机性;
二. 一维空间中的随机数
一维空间本质上就是一条线,因为线密度是均匀分布的,所以等可能地随机取点本质上就是生成这一条线段范围内的随机数,没什么难度。
也许读者想问如果是一条曲线该怎么取,其实曲线的本质是二维空间中宽度为零的图形,并不该在一维中讨论。并且曲线的随机需要更高级的数值技术,此处不作讨论。
三. 二维空间中的随机数
二维空间本质上是一个均匀分布的平面,我们讨论一块封闭的平面。若平面是一个标准的矩形,只需要在 x 和 y 的范围内各自生成随机数即可;若平面是一个圆,阁下该如何应对呢?
这是经典的错误答案:
在半径为 1 的圆内等概率地取点,可以使用极坐标方式生成随机点。这个方法可以确保在圆内的每个区域有相同的概率被选中。下面是一种常见的方法:
(1)生成一个随机角度:从 0 到 2π 之间随机选择一个角度,表示极坐标系中的角度。
(2)生成一个随机半径:从 0 到 1 之间随机选择一个值,表示极坐标系中的半径。这会覆盖圆的整个范围。
(3)将极坐标转换为直角坐标:使用所选的角度和半径,将极坐标转换为平面上的直角坐标。
这样,你就获得了圆内的一个等概率随机点。
因为极坐标在生成随机半径时所有长度是等可能的,因此
P
(
0
<
r
<
a
)
=
P
(
a
<
r
<
2
a
)
P(0 < r < a) = P(a < r < 2a)
P(0<r<a)=P(a<r<2a),其中
0
<
a
<
R
/
2
0 < a < R/2
0<a<R/2,于是有
P
(
S
1
)
/
P
(
S
2
)
=
1
/
2
P(S1) / P(S2) = 1 / 2
P(S1)/P(S2)=1/2:
但根据古典概型,
P
(
S
1
)
/
P
(
S
2
)
=
π
a
2
/
4
π
a
2
P(S1) / P(S2) = \pi a^2 / 4 \pi a^2
P(S1)/P(S2)=πa2/4πa2,与之矛盾,因此这种取点法并不是等可能的。
要想等可能地在圆中取点,最简单直接的办法就是在 x 和 y 的范围内各自生成随机数,若该点在圆的范围内则保留,否则舍弃。但该方法会造成很多无谓的操作,下面介绍 “极坐标投影法”:
(1)生成两个随机数
u
u
u 和
v
v
v,范围都是 [0, 1];
(2)计算
θ
=
2
π
∗
u
\theta = 2\pi * u
θ=2π∗u,这是极坐标的角度;
(3)计算
r
=
v
r = \sqrt v
r=v,这是极坐标的半径;
(4)将极坐标转换为直角坐标:
x
=
r
cos
θ
x = r \cos \theta
x=rcosθ,
y
=
r
sin
θ
y = r \sin \theta
y=rsinθ;
对 v v v 取平方根是因为在极坐标投影法中,希望生成的点在圆内均匀分布,而不是在整个极坐标平面上均匀分布。如果直接使用 v v v 作为半径,会导致在圆心附近的点聚集,而远离圆心的点分布稀疏。通过对 v v v 取平方根,可以确保半径的分布更加均匀。这是因为在 [0, 1] 区间内,取平方根后的值越接近 1,密度越高,越接近 0,密度越低。这就保证了生成的半径在 [0, 1] 区间内均匀分布,而不是线性分布。
四. 三维空间中的随机数
三维空间就是一个空间,若想在球中等可能随机取一个点,可以仿照极坐标投影法,过程如下:
(1)生成三个随机数
u
u
u、
v
v
v 和
t
t
t,范围都是 [0, 1];
(2)计算
θ
=
2
π
∗
u
\theta = 2\pi * u
θ=2π∗u,这是极角,即点坐标与
z
z
z 轴之间的夹角;
(3)计算
ϕ
=
2
π
∗
v
\phi = 2\pi * v
ϕ=2π∗v,这是方位角,即点坐标在 xOy 平面的投影与
x
x
x 轴之间的夹角;
(4)计算
r
=
t
3
r = \sqrt [3] {t}
r=3t,这是极坐标的半径;
(5)将极坐标转换为直角坐标:
x
=
r
sin
θ
cos
ϕ
x = r \sin \theta \cos \phi
x=rsinθcosϕ,
y
=
r
sin
θ
sin
ϕ
y = r \sin \theta \sin \phi
y=rsinθsinϕ,
z
=
r
cos
θ
z = r \cos \theta
z=rcosθ;
若想在球 面 上等可能随机取一个点,该怎么做呢?
球面上的点关于 θ \theta θ 和 ϕ \phi ϕ 都是等可能的,因此可以直接对 θ \theta θ 和 ϕ \phi ϕ 线性随机即可:
(1)生成两个随机数 u u u 和 v v v,范围都是 [0, 1];
(2)计算 θ = 2 π ∗ u \theta = 2\pi * u θ=2π∗u,这是极角;
(3)计算 ϕ = 2 π ∗ v \phi = 2\pi * v ϕ=2π∗v,这是方位角;
(4)将极坐标转换为直角坐标: x = sin θ cos ϕ x = \sin \theta \cos \phi x=sinθcosϕ, y = sin θ sin ϕ y = \sin \theta \sin \phi y=sinθsinϕ, z = cos θ z = \cos \theta z=cosθ;