Каково истинное максимальное (и минимальное) значение Random.nextGaussian()?

Теоретически, границы для nextGaussian подразумевают положительную и отрицательную бесконечность. Но поскольку Random.nextDouble, который используется для вычисления гауссовского случайного числа, не подходит бесконечно близко к 0 и 1, существует практический предел для nextGaussian. И Random.next также не является абсолютно равномерным распределением.

Было высказано предположение, что максимум должен быть около 2,2042 * 10 ^ 17 и связан с 53-битным сдвигом nextDouble (эталон), но это, скорее всего, только верхняя граница.

Ответ, вероятно, зависит от распределения Random.next и точной реализации StrictMath.sqrt и StrictMath.log. Я не мог найти много информации об этом.

И да, я знаю, что внешние значения крайне маловероятны, но это может быть актуально, например, в контексте манипуляций с ГСЧ в играх.

Ответ 1

Случайная реализация

Самая важная вещь, которую вы должны знать для этого ответа, это реализация 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 или 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, остальные три значения содержат очень похожую логику. Я собираюсь построить график v1 на оси x, v2 на оси y и v1 * multiplier на оси z.

Graph

Наши глаза сразу же прыгают к максимальной точке при v1= 0, v2= 0, v1 * multiplier= бесконечность. Но если вы заметили в цикле do-while, это явно запрещает эту ситуацию. Следовательно, из графика ясно, что фактическое значение v1_max должно иметь чуть более высокое значение v1, но не намного выше. Также следует отметить, что для любого значения v1 > 0 максимальный v1 * multiplier равен v2= 0.

Наш метод найти v1_max - подсчитать v1 с нуля (или, точнее, nextDouble который сгенерировал его с 0,5, с шагом в 2 ^ -53 согласно реализации nextDouble). Но, просто зная v1, как мы можем получить другие переменные и v1 * multiplier для этого v1?

Реверсивный следующийДвойной

Оказывается, что знания выходных nextDouble вызова nextDouble достаточно для определения начального числа объекта Random который сгенерировал его в то время. Интуитивно nextDouble, что это потому, что, глядя на реализацию 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 а не начальные nextDouble.

Объединяя все вместе

Схема алгоритма выглядит следующим образом:

  1. Инициализируйте nd1 (обозначает nextDouble 1) до 0.5
  2. Пока верхняя граница и наш текущий v1_max не пересеклись, повторите шаги 3-7
  3. Увеличение nd1 на 2 ^ -53
  4. Вычислить seed из nd1 (если он существует) и сгенерировать nd2, v1, v2 и s
  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, вы должны xor их с помощью множителя Random, 0x5deece66dL (что уже было сделано для вас в таблице выше).

Ответ 2

Так что все, что я скажу здесь, является чисто теоретическим, и я все еще работаю над программой 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;

   }

 }

Самая интересная часть должна быть в конце, [return 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" - это двойной эпсилон, который равен 2 ^ (-1022). Возвращаясь назад, чтобы получить такое число, nextDouble необходимо будет сгенерировать (2 ^ (-1022) + 1)/2.

Это... очень, очень, очень тревожно. Я не эксперт, но я уверен, что многие биты будут потеряны, и следует ожидать ошибок с плавающей точкой.

Вероятно (совершенно определенно) невозможно, чтобы nextDouble генерировал такое значение, но цель состоит в том, чтобы найти значение, максимально приближенное к этому числу.

Просто для удовольствия, давайте сделаем полную математику, чтобы найти ответ. StrictMath.log() реализован как натуральный лог. Я не смотрел в это точность, но позвольте предположить, что не было никаких ограничений на этом уровне. Самый высокий следующий гауссов будет рассчитан как...

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

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

Хотите верьте, хотите нет, я едва мог найти калькулятор, который бы принимал такие маленькие числа, но я, наконец, выбрал этот высокоточный калькулятор.

Подключив это уравнение,

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

Я получил,

1.273479378356503041913108844696651886724617446559145569961266215283953862086306158E + 311

Довольно большой, а? Ну... это определенно не будет таким большим... но это приятно принимать во внимание. Надеюсь, что мои рассуждения имеют смысл и не стесняйтесь указывать на любую ошибку, которую я сделал.

Как я уже сказал в начале, я работаю над программой, чтобы перебить все семена и найти фактическое минимальное значение. Я буду держать вас в курсе.

РЕДАКТИРОВАТЬ :

Извините за поздний ответ. После подбора 2 ^ 48 семян за 10 часов я нашел ТОЧНЫЕ ответы, аналогичные Earthcomputer.

Ответ 3

Моя ставка на 12.00727336061225.

Причины этого примерно совпадают с ответом KaptainWutax: принимая во внимание часть log(s)/s для множителя, цель должна состоять в том, чтобы сделать s как можно меньше. Это приходит с дополнительным ограничением, что v1 будет частью результата. Так по сути

  • v1 должен быть маленьким, чтобы s маленьким
  • v1 должен быть большим, чтобы конечный результат был большим

Но поскольку деление на s будет расти экспоненциально при приближении s к нулю, это перевесит вклад фактора v1.

Итак, подведем итог этой мысли:

Random#nextGaussian частью реализации 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

Ответ 4

вот и я

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

7.995084298635286 0.8744239748619776