随机实现
本答案所需了解的最重要的事情是Random.nextGaussian
方法的实现:
synchronized public double nextGaussian() {
if (haveNextNextGaussian) {
haveNextNextGaussian = false;
return nextNextGaussian;
} else {
double v1, v2, s;
do {
v1 = 2 * nextDouble() - 1;
v2 = 2 * nextDouble() - 1;
s = v1 * v1 + v2 * v2;
} while (s >= 1 || s == 0);
double multiplier = StrictMath.sqrt(-2 * StrictMath.log(s)/s);
nextNextGaussian = v2 * multiplier;
haveNextNextGaussian = true;
return v1 * multiplier;
}
}
接下来是Random.nextDouble
的实现:
public double nextDouble() {
return (double) (((long)(next(26)) << 27) + next(27)) / (1L << 53);
}
首先,我想提醒您
nextGaussian
每次生成 2 个值,而且根据您知道自上次设置种子以来调用了多少次
nextGaussian
,您可以针对奇偶数次调用使用略低的最大值。
从现在开始,我将称这两个最大值为 v1_max 和 v2_max,分别指代由
v1 * multiplier
或
v2 * multiplier
生成的值。
答案
现在让我们直入主题,稍后再解释:
| |Value |Seed* |
|------|------------------|---------------|
|v1_max|7.995084298635286 |97128757896197 |
|v2_max|7.973782613935931 |10818416657590 |
|v1_min|-7.799011049744149|119153396299238|
|v2_min|-7.844680087923773|10300138714312 |
* Seeds for v2 need to have nextGaussian called twice before you see the value listed.
深入了解nextGaussian
@KaptainWutax和@Marco13的答案已经详细介绍了同样的内容,但我认为通过图表更容易理解。让我们关注v1_max,其他三个值具有非常相似的逻辑。我将在x轴上绘制v1
,在y轴上绘制v2
,在z轴上绘制v1 * multiplier
。
我们的眼睛立刻跳到最大点,在v1
=0,v2
=0,v1 * multiplier
=无穷大时。但是,如果您注意到do-while循环中,它明确禁止了这种情况。因此,从图表可以清楚地看出,实际的v1_max必须具有稍高的v1
值,但不会高得多。还值得注意的是,对于任何v1
值> 0,最大的v1 * multiplier
在v2
=0处。
我们找到v1_max的方法是从零开始计算v1
(或者更具体地说,从0.5开始计算生成它的nextDouble
,按2^-53的步长递增,根据nextDouble
的实现)。但是,仅仅知道v1
,如何获得其他变量以及该v1
的v1 * multiplier
呢?
反向nextDouble
事实证明,知道nextDouble
调用的输出足以确定生成它的Random
对象的种子。直观地说,这是因为查看nextDouble
的实现,它“看起来”应该有2^54个可能的输出-但是Random
的种子只有48位。此外,可以比暴力更快地恢复此种子。
我最初尝试了一种基于直接使用next(27)
获取种子位的方法,然后暴力破解剩余的21位,但这被证明太慢而无法使用。然后SicksonFSJoe给了我一种从单个nextDouble
调用中提取种子的更快速的方法。请注意,要理解此方法的细节,您将必须了解Random.next
的实现和一些模算术。
private static long getSeed(double val) {
long lval = (long) (val * (1L << 53));
long a = lval >> 27;
long b = lval & ((1 << 27) - 1);
long lhs = ((b << 21) - 0xb - (a << 22) * 0x5deece66dL) & 0xffffffffffffL;
for (long k = 65535; k != 376; k = k == 65535 ? 0 : k + 1) {
long rem = (0x5deece66dL - (lhs + (k << 48))) % 0x5deece66dL;
long d = (rem + 0x5deece66dL) % 0x5deece66dL;
if (d < (1 << 21)) {
long c = lhs + d;
c *= 0xdfe05bcb1365L;
c &= 0xffffffffffffL;
if (c < (1 << 22)) {
long seed = (a << 22) + c;
seed = ((seed - 0xb) * 0xdfe05bcb1365L) & 0xffffffffffffL;
return seed;
}
}
}
return Long.MAX_VALUE;
}
现在我们可以从
nextDouble
中获取种子,因此迭代
v1
值而不是种子是有意义的。
将所有内容汇总
算法概述如下:
- 将
nd1
(代表nextDouble
1)初始化为0.5
- 当上限和当前的
v1_max
未越过时,重复步骤3-7
- 将
nd1
增加2 ^ -53
- 从
nd1
计算seed
(如果存在),并生成nd2
、v1
、v2
和s
- 检查
s
的有效性
- 生成高斯分布,与
v1_max
进行比较
- 通过假设
v2
=0来设置新的上限
以下是Java实现。如果您想要验证我提供的值,可以自行进行验证。
public static void main(String[] args) {
double upperBound;
double nd1 = 0.5, nd2;
double maxGaussian = Double.MIN_VALUE;
long maxSeed = 0;
Random rand = new Random();
long seed;
int i = 0;
do {
nd1 += 0x1.0p-53;
seed = getSeed(nd1);
double v1, v2, s;
v1 = 2 * nd1 - 1;
if (seed != Long.MAX_VALUE) {
rand.setSeed(seed ^ 0x5deece66dL);
rand.nextDouble();
nd2 = rand.nextDouble();
v2 = 2 * nd2 - 1;
s = v1 * v1 + v2 * v2;
if (s < 1 && s != 0) {
double gaussian = v1 * StrictMath.sqrt(-2 * StrictMath.log(s) / s);
if (gaussian > maxGaussian) {
maxGaussian = gaussian;
maxSeed = seed;
}
}
}
upperBound = v1 * StrictMath.sqrt(-2 * StrictMath.log(v1 * v1) / (v1 * v1));
if (i++ % 100000 == 0)
System.out.println(maxGaussian + " " + upperBound);
} while (upperBound > maxGaussian);
System.out.println(maxGaussian + " " + maxSeed);
}
需要注意的最后一个问题是,此算法将为您获取Random
的内部种子。要在setSeed
中使用它,您必须将其与Random
的乘数0x5deece66dL
异或(在上面的表格中已经为您完成了)。
Random
类而言,是的。但对于我所查看的StrictMath
类,没有源代码(https://docs.oracle.com/javase/7/docs/api/java/lang/StrictMath.html)。此外,它并不仅仅给出答案,需要进行相当多的分析,其中并非所有部分我都能理解。 - Fabian Röling