static inline float __cdecl LCG_rand(float& state)...

Créé le : 13 septembre 2025

Répondu en utilisant GPT-5 par Chat01

Question

static inline float __cdecl LCG_rand(float& state)
{
state = std::fmaf(state, 1.618033988749895f, state * 0.5772156649f);
state = std::fabs(state - std::floor(state));
state = std::fmaf(state, state + 1.0f, 0.0f);
state = std::fabs(state - std::floor(state));
return state;
} у меня есть проблемы с этим генератором, мне кажется что каждый раз он генерирует одно и то же число, я передаю seed как целое число примерно 82587062, каждый раз seed немного разный но порядок величины всегда такой - на выходе я должен получить случайное число в диапазоне 0 - 1, заметь в этой функции меняется состояние - так и должно быть, найди в чём проблема одинаковых значений - предложи мне решение, мне нужен генератор, который будет давать случайные числа в диапазоне 0 - 1 и при этом чтобы состояние изменялось и следующее состояние использовалось в следующей генерации, заметь в чём сложность - само возвращаемое значение должно быть в диапазоне 0 - 1, но seed - это состояние которое примерно имеет порядок как у этого числа 82587062 и нужно чтобы состояние state менялось на число примерно похожее по порядку, заметь что принимаемое значение имеет тип float, хоть это и целое число - так надо, возвращаемое значение также типа float - и это менять нельзя, поэтому ты не можешь использовать трюки с битовыми сдвигами целых чисел как в XOR_RAND

Réflexion

Analyzing float precision and bug

When examining how the user’s 82,587,062 seed is represented, it seems that single precision (with a 24-bit mantissa) can’t precisely represent integers above about 16.7 million. At 82 million, the float value has a spacing of 8, and certain calculations like using constants seem to introduce instability.

Examining float rounding and behavior

Starting with different seeds that differ by less than 8 leads to rounding to the same float. When multiplying the state by 2.1952 (approximated from phi and gamma), large initial seeds (like 8e7) lose precision, and the fractional part can be dominated by lower bits. This likely causes similar outcomes for seeds differing by 8.

Understanding precision loss in large floats

When dealing with large floats, the fractional part may lose precision due to catastrophic cancellation. For floats with magnitude greater than 2^24, they are treated as integers—losing any precise fractional part. Even operations like multiplication still round, and fractional parts may align with the float’s quantization, losing accuracy.

Evaluating float seed behavior

When multiplying by large floats, the value becomes rounded to an integer due to floating-point precision, resulting in a fractional part of zero. This causes the state to stay at 0, which leads to always outputting 0. The issue is that floats above 2^24 lose fractional details. We need to normalize the seed prior to operations.

Maintaining state magnitude

To keep the state at a large magnitude (8e7), I propose using a modulus for the linear congruential generator (LCG) and promoting intermediate calculations to double precision to preserve fractional details. This way, the scale can be returned to the desired range and maintain the correct entropy without losing precision during updates.

Thought for 51s

Réponse

Проблема не в «рандомности» формул, а в потере дробной части при больших значениях float.

  • float имеет ~24 бита мантиссы. Для чисел порядка 8.3e7 шаг квантования уже 8 (и больше при умножении), т.е. большинство разных seed сводятся к одному и тому же представимому значению.
  • Затем вы делаете state = fabs(state - floor(state)). Для больших float после умножений результат округляется к целому, и floor(x) == x, поэтому дробная часть становится 0. Дальше вся цепочка детерминированно «залипает» на одном значении.

Нужно либо считать дробную часть (и всю «логику») в double, либо работать по модулю с масштабом порядка 1e8, а уже потом возвращаться в float.


Решение 1 (минимальные правки вашего кода)

  • Все вычисления делаем в double, дробную часть берём в double.
  • Возвращаем число r в [0, 1).
  • Состояние state после вызова снова делаем «большим» (порядка 1e8), чтобы следующий вызов начинался с крупного числа, как вы хотите.
cpp
#include <cmath> #include <cfloat> static inline float LCG_rand(float& state) { // Подстрахуемся от нулевого состояния if (!std::isfinite(state) || state == 0.0f) state = 1.0f; // Работаем в double, чтобы не терять дробную часть double s = static_cast<double>(state); // Ваша "линейная" часть; эквивалентно s *= (1.618... + 0.5772...) s = std::fma(s, 1.618033988749895, s * 0.5772156649); // double fma // frac(s) s = s - std::floor(s); if (s <= 0.0) s = std::ldexp(1.0, -53); // крошечный сдвиг, чтобы не залипнуть в 0 // Ещё один шаг перемешивания (как у вас) s = std::fma(s, s + 1.0, 0.0); s = s - std::floor(s); // снова frac, итог в [0,1) // r — возвращаемое значение в [0,1) float r = static_cast<float>(s); // Хотите, чтобы СООТВЕТСТВУЮЩЕЕ состояние было снова "большим": // масштабируем в ~1e8 (и немного "шевелим", чтобы не нулиться) const double SCALE = 1.0e8; double next_state = s * SCALE; // Чуть-чуть перемешаем и избежим точного целого next_state = std::nextafter(next_state + 0.5, next_state + 1.0); state = static_cast<float>(next_state); // остаётся порядка 1e8 return r; }

Что это даёт

  • Возвращаемое значение всегда в [0, 1).
  • state после вызова снова «большое» (≈ 1e8) и меняется каждый раз.
  • Нет залипания из-за потери дробной части в float, потому что всё «мод 1» делается в double.

Решение 2 (надёжнее: LCG по модулю ~1e8)

Если хотите классический «модульный» генератор со стабильным обновлением большого состояния:

cpp
#include <cmath> static inline float LCG_rand(float& state) { // Параметры LCG (можно менять, лишь бы a и m были взаимно просты) constexpr double M = 100000007.0; // ~1e8, простое constexpr double A = 48271.0; // множитель constexpr double C = 0.0; // инкремент (можно 0 для Lehmer-стиля) // Восстанавливаем корректное начальное состояние double s = std::fmod(static_cast<double>(state), M); if (s <= 0.0) s = 1.0; // Следующее состояние по модулю M — в double, чтобы не терять точность s = std::fmod(A * s + C, M); if (s < 0.0) s += M; // Обновляем "большое" состояние (порядка 1e8) state = static_cast<float>(s); // Возвращаем нормированное значение в [0,1) return static_cast<float>(s / M); }

Плюсы этого варианта

  • Простая и предсказуемая математика.
  • state всегда порядка 1e8, как вы просите.
  • Возвращаемое значение — ровно state / M в [0,1).
  • Без целочисленных битовых трюков; только double-арифметика.

Примечание: из-за того, что state хранится в float, очень «тонкая» воспроизводимость последовательности при длинных сериях не гарантируется (потому что float округляет при сохранении). Но для «слегка разные сиды ≈8e7 → разные выходы» это надёжно работает и не залипает.


Коротко: почему было «всегда одно и то же»

  • При seed ≈ 8.3e7 у float нет битов, чтобы хранить дробную часть после умножений → frac(x) даёт 0 → дальше всё повторяется одинаково.

Что выбрать

  • Хотите сохранить вашу формулу — берите Решение 1 (double для дробной части + рескейл состояния).
  • Хотите классический, простой и стабильный генератор — берите Решение 2 (LCG по модулю ~1e8).

Если нужно, подберу другие параметры A, M или добавлю быстрый «соляной» хеш входного seed, чтобы ещё сильнее развести близкие сиды.

Partager cette Q&R