Каково истинное максимальное (и минимальное) значение 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
.
Объединяя все вместе
Схема алгоритма выглядит следующим образом:
- Инициализируйте
nd1
(обозначает nextDouble
1) до 0.5 - Пока верхняя граница и наш текущий v1_max не пересеклись, повторите шаги 3-7
- Увеличение
nd1
на 2 ^ -53 - Вычислить
seed
из nd1
(если он существует) и сгенерировать 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) { // 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