Random.nextGaussian()的真实最大(和最小)值是多少?

时间:2019-02-15 15:39:58

标签: java random

理论上,nextGaussian的边界是正无穷大。但是,由于用于计算高斯随机数的Random.nextDouble并非无限接近0和1,因此nextGaussian有一个实际的限制。而且Random.next也不是完全均匀的分布。

从理论上讲,最大值应该约为2.2042 * 10 ^ 17,并且与nextDoublereference)的53位移位有关,但这可能只是一个上限。

答案可能取决于Random.next的分布以及StrictMath.sqrtStrictMath.log的确切实现。我找不到任何有关这两个信息。

是的,我知道外部值极不可能,但是它可能是相关的,例如在游戏中进行RNG操纵的情况下。

4 个答案:

答案 0 :(得分:7)

因此,我在这里所说的一切纯粹是理论上的,我仍在研究GPU程序以扫描整个种子库。

nextGaussian()方法就是这样实现的。

private double nextNextGaussian;
private boolean haveNextNextGaussian = false;

 public double nextGaussian() {

   if (haveNextNextGaussian) {

     haveNextNextGaussian = false;
     return nextNextGaussian;

   } else {

     double v1, v2, s;

     do {
       v1 = 2 * nextDouble() - 1;   // between -1.0 and 1.0
       v2 = 2 * nextDouble() - 1;   // between -1.0 and 1.0
       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;

   }

 }

最有趣的部分必须在最后,[返回v1 *乘数]。由于v1不能大于1.0D,因此我们需要找到一种增加乘法器大小的方法,该方法实现如下。

double multiplier = StrictMath.sqrt(-2 * StrictMath.log(s)/s);

唯一的变量是“ s”,可以确定“ s”越小,乘数将越大。都好?我们继续吧。

 do {
   v1 = 2 * nextDouble() - 1;   // between -1.0 and 1.0
   v2 = 2 * nextDouble() - 1;   // between -1.0 and 1.0
   s = v1 * v1 + v2 * v2;
 } while (s >= 1 || s == 0);

这告诉我们“ s”必须属于设置的[0,1 [],并且我们正在寻找的最小值稍大于零。用“ v1”和“ v2”的平方和声明“ S”。为了获得最小的理论值,v2必须为零,而v1必须尽可能小。为什么是“理论的”?因为它们是从nextDouble()调用生成的。无法保证种子库包含这两个连续的数字。

让我们玩得开心!

可以容纳的最低值“ v1”是double的epsilon,即2 ^(-1022)。回到前面,要获得这样的数字,nextDouble需要生成(2 ^(-1022)+1)/ 2。

那是...非常非常令人不安。我不是专家,但我可以肯定会丢失很多位,并且会出现浮点错误。

nextDouble可能(最绝对)不可能生成这样的值,但是目标是找到一个尽可能接近该数字的值。

仅出于乐趣,让我们做完整的数学运算以找到答案。 StrictMath.log()被实现为自然日志。我没有研究它的精度,但让我们假设在该级别上没有任何限制。最高的nextGaussian将被计算为...

= (-2 * ln(v1 * v1) / (v1 * v1)) * v1 
= (-2 * ln(EPSILON^2) / (EPSILON^2)) * EPSILON

where EPSILON is equal to 2^(-1022).

信不信由你,我几乎找不到可以接受这么小的数字的计算器,但是我最终选择了this high precision calculator

通过插入此等式,

  

(-2 * ln((2 ^(-1022))^ 2)/((2 ^(-1022))^ 2))*(2 ^(-1022))

我知道了

  

1.273479378356503041913108844696651886724617446559145569961266215283953862086306158E + 311

很大吧?好吧...肯定不会那么大...但是考虑到这一点很好。希望我的推理是有道理的,不要害羞地指出我犯的任何错误。

正如我在一开始所说的那样,我正在制定一个程序,以暴力破解所有种子并找到实际的最低价值。我会及时通知您。

编辑:

对不起,您的回复很晚。在大约10小时内对2 ^ 48颗种子进行强行暴力攻击后,我发现了与Earthcomputer完全相同的答案。

答案 1 :(得分:5)

随机实施

此答案最重要的是Random.nextGaussian的实现:

synchronized public double nextGaussian() {
    // See Knuth, ACP, Section 3.4.1 Algorithm C.
    if (haveNextNextGaussian) {
        haveNextNextGaussian = false;
        return nextNextGaussian;
    } else {
        double v1, v2, s;
        do {
            v1 = 2 * nextDouble() - 1; // between -1 and 1
            v2 = 2 * nextDouble() - 1; // between -1 and 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.

近距离观察下一个高斯

@KaptainWutax和@ Marco13的答案已经详细介绍了相同的事物,但是我认为在图表上看到事物可以使事情更清晰。让我们关注v1_max,其他三个值具有非常相似的逻辑。我要在x轴上绘制v1,在y轴上绘制v2,在z轴上绘制v1 * multiplier

Graph

我们的眼睛立即跳到最高点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为步长递增,如下所示:根据{{​​1}}的实现)。但是,仅了解nextDouble,我们如何获得其他变量,以及该v1的{​​{1}}?

撤消nextDouble

事实证明,知道v1 * multiplier调用的输出足以确定当时生成它的v1对象的种子。直观地讲,这是因为查看nextDouble实现时,“看起来”应该有2 ^ 54个可能的输出-但是Random的种子只有48位。而且,可以比蛮力更快地恢复该种子。

我最初尝试了一种天真的方法,该方法基于直接使用nextDouble来获取种子的位,然后对其余的21位进行强行强制,但是事实证明这太慢了,无法使用。然后SicksonFSJoe给了我更快的方法来从单个Random调用中提取种子。请注意,要了解此方法的详细信息,您将必须了解next(27)的实现以及一些模块化算法。

nextDouble

现在我们可以从Random.next获取种子了,有意义的是我们可以遍历private static long getSeed(double val) { long lval = (long) (val * (1L << 53)); // let t = first seed (generating the high bits of this double) // let u = second seed (generating the low bits of this double) long a = lval >> 27; // a is the high 26 bits of t long b = lval & ((1 << 27) - 1); // b is the high 27 bits of u // ((a << 22) + c) * 0x5deece66d + 0xb = (b << 21) + d (mod 2**48) // after rearranging this gives // (b << 21) - 11 - (a << 22) * 0x5deece66d = c * 0x5deece66d - d (mod 2**48) // and because modular arithmetic // (b << 21) - 11 - (a << 22) * 0x5deece66d + (k << 48) = c * 0x5deece66d - d long lhs = ((b << 21) - 0xb - (a << 22) * 0x5deece66dL) & 0xffffffffffffL; // c * 0x5deece66d is 56 bits max, which gives a max k of 375 // also check k = 65535 because the rhs can be negative for (long k = 65535; k != 376; k = k == 65535 ? 0 : k + 1) { // calculate the value of d long rem = (0x5deece66dL - (lhs + (k << 48))) % 0x5deece66dL; long d = (rem + 0x5deece66dL) % 0x5deece66dL; // force positive if (d < (1 << 21)) { // rearrange the formula to get c long c = lhs + d; c *= 0xdfe05bcb1365L; // = 0x5deece66d**-1 (mod 2**48) c &= 0xffffffffffffL; if (c < (1 << 22)) { long seed = (a << 22) + c; seed = ((seed - 0xb) * 0xdfe05bcb1365L) & 0xffffffffffffL; // run the LCG forwards one step return seed; } } } return Long.MAX_VALUE; // no seed } 值而不是种子。

将所有内容放在一起

算法概述如下:

  1. nextDouble(代表v1 1)初始化为0.5
  2. 虽然上限和我们当前的v1_max还没有越过,请重复步骤3-7
  3. nd1增加2 ^ -53
  4. nextDouble(如果存在)中计算nd1,并生成seednd1nd2v1
  5. 检查v2的有效性
  6. 生成高斯,与v1_max比较
  7. 假设s = 0来设置新的上限

这是Java实现。您可以根据需要验证我上面提供的值。

s

最后一个需要注意的问题是,该算法将为您提供v2的内部种子。要在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) { // not no seed rand.setSeed(seed ^ 0x5deece66dL); rand.nextDouble(); // nd1 nd2 = rand.nextDouble(); v2 = 2 * nd2 - 1; s = v1 * v1 + v2 * v2; if (s < 1 && s != 0) { // if not, another seed will catch it 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(在上表中已为您完成)进行异或。

答案 2 :(得分:4)

我的赌注在 12.00727336061225

其背后的原因大致与answer by KaptainWutax相似:考虑乘数的log(s)/s部分,目标必须是使s尽可能小。这带有附加约束,即v1将成为结果的一部分。所以本质上

  • v1必须很小,所以s应该很小
  • v1必须很大,以便最终结果很大

但是,由于s的除法将随着s接近零而呈指数增长,因此这将超过因素v1的贡献。

所以总结一下思路:

实施Random#nextGaussian的必要部分是:

double nextGaussian() {
    double v1, v2, s;
    do {
        v1 = 2 * nextDouble() - 1; // between -1 and 1
        v2 = 2 * nextDouble() - 1; // between -1 and 1
        s = v1 * v1 + v2 * v2;
    } while (s >= 1 || s == 0);
    double multiplier = StrictMath.sqrt(-2 * StrictMath.log(s)/s);
    return v1 * multiplier;
}

Random#nextDouble方法的实现方式如下:

double nextDouble() {
    return (((long)next(26) << 27) + next(27)) / (double)(1L << 53);
}

其中next(n)返回一个整数,其中随机设置最低的n位。

为了最大化nextGaussian的价值,人们可以争论:

  • s的值必须与0.0尽可能接近(但不能与0.0接近)
  • 因此,v2的“最佳”值将是0.0,而v1的“最佳”值将是可以作为{{1 }}
  • 为了拥有2 * nextDouble() - 1,我们假设v2==0.0调用中的随机位是nextDouble-在这种情况下,0b10000000000000000000000000000000000000000000000000000L将返回nextDouble ,而0.5将是v2
  • 将导致0.0的最小有效值的位将是v1-最后只有一个令人讨厌的位,导致0b10000000000000000000000000000000000000000000000000001L返回nextDouble,产生0.5000000000000001的{​​{1}}值
  • 鉴于这些值,2.220446049250313E-16将是v1,乘数将是s,最终结果将是

    12.00727336061225

这里是一个示例,您可以在其中使用4.930380657631324E-32调用返回的可能的位组合,这些位组合是此处整个计算的基础。也许有人发现一种组合可以产生更高的价值...?

5.4075951832589016E16

输出如上所述,

Random#next

答案 3 :(得分:-3)

你去哪里

setprecision fixed setw

7.995084298635286 0.8744239748619776