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

问题描述 投票:16回答:4

从理论上讲,nextGaussian的界限是正负无穷大。但由于用于计算高斯随机数的Random.nextDouble不会无限接近0和1,因此nextGaussian存在实际限制。而Random.next也不是一个完美的均匀分布。

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

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

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

java random
4个回答
5
投票

Random Implementation

对于这个答案,您必须知道的最重要的事情是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生成的。

The Answer

有了这一点,让我们直接切入追逐并稍后解释:

|      |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.

A Closer Look at 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 * multiplierv2 = 0。

我们找到v1_max的方法是从零开始计算v1(或者更具体地说,计算从0.5生成它的nextDouble,按照nextDouble的实现递增2 ^ -53的步长)。但是,只要知道v1,我们如何获得其他变量,以及v1 * multiplierv1

Reversing 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));
    // 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
}

现在我们可以从nextDouble获得种子,我们可以迭代v1值而不是种子。

Bringing it All Together

算法概要如下:

  1. nd1(代表nextDouble 1)初始化为0.5
  2. 虽然上限和我们当前的v1_max没有交叉,但重复步骤3-7
  3. 增加nd1 2 ^ -53
  4. seed(如果存在)计算nd1,并生成nd2v1v2s
  5. 检查s的有效性
  6. 生成高斯,与v1_max进行比较
  7. 假设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) { // 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中使用它,你必须使用Random的乘数0x5deece66dL(在上表中已经为你完成)对它们进行xor。


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 [数字集,并且我们要寻找的最低值只是略大于零。 “S”用“v1”和“v2”的平方和声明。要获得最小的理论值,v2需要为零,v1需要尽可能小。为什么“理论”?因为它们是从nextDouble()调用生成的。无法保证种子库包含这2个连续数字。

让我们玩得开心吧!

最低值“v1”可以保持双倍的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,2734,793,735,356,5030,191,110,888,446,965,651,867,676,617,744,455,955,565,996,126,615,158,630,838,306,158 J + 311

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

正如我在开始时所说的,我正在制定一个程序来强制所有种子并找到实际的最低值。我会及时通知你的。

编辑:

回复晚了非常抱歉。在大约10个小时内煮熟2 ^ 48粒种子后,我发现了与Earthcomputer完全相同的答案。


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的“最佳”值将是2 * nextDouble() - 1可能导致的最小值。
  • 为了得到v2==0.0,我们假设nextDouble调用中的随机位是0b10000000000000000000000000000000000000000000000000000L - 在这种情况下,nextDouble将返回0.5,而v2将是0.0
  • 那将导致v1最小有效值的位将是0b10000000000000000000000000000000000000000000000000001L - 最后只有一个恼人的位,导致nextDouble返回0.5000000000000001,为2.220446049250313E-16产生值v1
  • 给定这些值,s将是4.930380657631324E-32,乘数将是5.4075951832589016E16,最终结果将是 12.00727336061225

下面是一个示例,您可以在其中使用Random#next调用返回的位组合,这些调用是此处整个计算的基础。也许有人发现产生更高价值的组合......?

public class LargestNextGaussian
{
    public static void main(String[] args)
    {
        // Random#nextDouble is implemented as 
        //   (((long)next(26) << 27) + next(27)) / (double)(1L << 53)
        // The "baseValue" here refers to the value that
        // is obtained by combining the results of the 
        // two calls to "next"

        long baseValueForV1 = 
            0b10000000000000000000000000000000000000000000000000001L;
        double valueForV1 = 
            baseValueForV1 / (double)(1L << 53);

        long baseValueForV2 = 
            0b10000000000000000000000000000000000000000000000000000L;
        double valueForV2 = 
            baseValueForV2 / (double)(1L << 53);

        // As of Random#nextGaussian:
        double v1, v2, s;
        do {
            v1 = 2 * valueForV1 - 1;
            v2 = 2 * valueForV2 - 1;
            s = v1 * v1 + v2 * v2;
        } while (s >= 1 || s == 0);
        double multiplier = StrictMath.sqrt(-2 * StrictMath.log(s)/s);
        double result = v1 * multiplier;

        System.out.println("baseValueForV1 " + Long.toBinaryString(baseValueForV1));
        System.out.println("baseValueForV2 " + Long.toBinaryString(baseValueForV2));
        System.out.println("valueForV1     " + valueForV1);
        System.out.println("valueForV2     " + valueForV2);
        System.out.println("v1             " + v1);
        System.out.println("v2             " + v2);
        System.out.println("s              " + s);
        System.out.println("multiplier     " + multiplier);
        System.out.println("result         " + result);
        System.out.println();
    }
}

输出如上所述:

baseValueForV1 10000000000000000000000000000000000000000000000000001
baseValueForV2 10000000000000000000000000000000000000000000000000000
valueForV1     0.5000000000000001
valueForV2     0.5
v1             2.220446049250313E-16
v2             0.0
s              4.930380657631324E-32
multiplier     5.4075951832589016E16
result         12.00727336061225

-3
投票

你走了:

long seed=97128757896197L; Random r= new Random(seed ); System.out.println(r.nextGaussian()); System.out.println(r.nextGaussian());

7.995084298635286 0.8744239748619776

© www.soinside.com 2019 - 2024. All rights reserved.