Skip to Content

§3. Математические датчики: станок, который сам плодит цифру

Железо отпало, тетрадь со случайными цифрами жрёт память и таскать её западло. Нормальному пацану нужен станок прямо в процессоре: коротенький алгоритм, который сам, из головы, штампует псевдослучайные числа — быстро, пачками, и чтоб при нужде можно было отмотать тот же расклад назад (задал стартовое число — получил ту же серию). Вот такие станки и называют математическими датчиками.

Устроены они обычно как рекуррентные алгоритмы: следующее число лепится из предыдущего. Задал старт — и понеслась цепочка y1,y2,y3,…y_1, y_2, y_3, \dots, где каждое вытекает из соседа слева. Коротко, детерминированно, воспроизводимо. Разберём от простого к рабочему.

Метод середины квадрата: красиво, но гнилой

Первый заход — метод середины квадрата (Джон фон Нейман, 1946). Схема детская:

  1. Берём четырёхзначное число k0k_0.
  2. Возводим в квадрат (получаем до восьми цифр).
  3. Выдираем средние четыре цифры — это k1k_1.
  4. Нормируем: y1=k1⋅10−4y_1 = k_1 \cdot 10^{-4} (то есть k1/10000k_1/10000 — загоняем в [0,1)[0,1)).
  5. С k1k_1 повторяем всё заново, и так далее.

Пример от Лагутина. Старт k0=8473k_0 = 8473. Квадрат: k02=71 791 729k_0^2 = 71\,791\,729. Средние четыре цифры — 79177917, значит k1=7917k_1 = 7917 и y1=0,7917y_1 = 0{,}7917. Дальше k12=62 678 889k_1^2 = 62\,678\,889, середина — 67886788, выходит y2=0,6788y_2 = 0{,}6788. И покатилось.

Погоняй сам — жми Run:

Загрузка редактора…

Красиво? Красиво. Только датчик гнилой, и вот где он палится:

  1. Старт решает всё. k0k_0 полностью задаёт всю цепочку — никакой свободы, вся «случайность» зашита в одно число.
  2. Зацикливается быстро — не позже чем через 10410^4 шагов (четырёхзначных чисел всего-то десять тысяч). Хуже: есть числа, которые воспроизводят сами себя и намертво застревают. Например 37922=14 379 2643792^2 = 14\,379\,264, середина — снова 37923792. Приехали, датчик встал на месте.
  3. Можно убить на ровном месте. Задашь неудачный старт (скажем, 10001000 или 00850085) — и датчик через пару шагов схлопывается в ноль и дальше плюёт одни нули. Полное фуфло.

Глянь на этот кидок своими глазами:

Загрузка редактора…

Вывод: красивая схема, но для серьёзного дела не катит. Нужен станок покрепче.

Линейный конгруэнтный датчик: рабочая лошадь

Вот это уже по-взрослому. Общая схема мультипликативного (и чуть шире — линейного конгруэнтного) датчика. Задаём стартовое k0k_0, множитель aa, приращение cc и делитель mm, и крутим по формуле:

{kn=(a⋅kn−1+c) mod m,yn=kn/m.(1)\begin{cases} k_n = (a \cdot k_{n-1} + c) \bmod m, \\[4pt] y_n = k_n / m. \end{cases} \tag{1}

Разбираем по кирпичикам, тут один новый значок:

  •  mod m\bmod m — это остаток от деления на mm. Поделил нацело, что осталось в хвосте — то и берём. Фишка в том, что остаток всегда сидит в диапазоне 0,1,…,m−10, 1, \dots, m-1 — то есть сколько ни умножай, результат всегда загнан в рамки. Как счётчик по кругу: добежал до mm — и обнулился.
  • a⋅kn−1+ca \cdot k_{n-1} + c — берём прошлое число, раскручиваем умножением на aa, добавляем cc.
  • knk_n — остаток этой движухи по модулю mm.
  • yn=kn/my_n = k_n / m — делим на mm, чтобы загнать в [0,1)[0,1).

При c=0c = 0 датчик называют мультипликативным — приращения нет, только умножение по кругу.

Теперь главное — какие aa и mm брать, чтобы датчик держал марку. Делитель выгодно брать простым: например m=231−1=2 147 483 647m = 2^{31} - 1 = 2\,147\,483\,647 (ровно влезает в 32-битное целое). А множитель aa подбирают так, чтобы цепочка k1,k2,…k_1, k_2, \dots пробежала все возможные значения от 11 до m−1m-1, прежде чем зациклиться — тогда период максимальный. Фишман и Мур после изучения кучи вариантов предложили, в частности, a=630 360 016a = 630\,360\,016.

Покрути датчик руками. Ниже — решётка пар (yn,yn+1)(y_n, y_{n+1}): берём соседние числа и кидаем точкой на плоскость. На пресете «Кривой» сразу видно палево — точки садятся на несколько прямых (датчик с грубым модулем предсказуем). На «Хорошем (MINSTD)» — ровная заливка, закономерности не видно:

Линейный конгруэнтный датчик: решётка пар (yₙ, yₙ₊₁)

a = 137, c = 187, m = 256. Грубый модуль — пары сразу садятся на несколько прямых.Параметры: a = 137, c = 187, m = 256, k₀ = 7. Период: 256. Первые yₙ: 0.027, 0.477, 0.020, 0.406, 0.387, 0.711…

RANDU: самый знаменитый кидок в истории датчиков

А теперь поучительная история про то, как целое поколение инженеров развели. В библиотеке SSP для машин IBM-360 жил датчик RANDU: m=231m = 2^{31}, множитель a=65539a = 65539, c=0c = 0. На пары (yn,yn+1)(y_n, y_{n+1}) он смотрится прилично — в плоскости палева нет. Им пользовались годами и не чуяли подвоха.

А подвох был жирнейший. Если брать числа тройками (y3n−2, y3n−1, y3n)(y_{3n-2},\, y_{3n-1},\, y_{3n}) и кидать их точками в трёхмерный куб, они не заполняют его, как положено честным независимым числам, а ложатся всего на 15 плоскостей:

9 y3n−2−6 y3n−1+y3n=k,k=−5,−4,…,9.9\,y_{3n-2} - 6\,y_{3n-1} + y_{3n} = k, \qquad k = -5, -4, \dots, 9.

То есть «случайные» тройки на самом деле сидят по струнке на полутора десятках плоскостей — в 2D этого не видать, а в 3D вылезает наружу. Классический случай: датчик выглядит чётко, пока не посмотришь под правильным углом.

Покрути куб. Переключи на RANDU и поймай ракурс вдоль диагонали — точки схлопнутся в плоскости. А на «хорошем» датчике облако заполняет куб ровно, без строя:

Тройки случайных чисел в кубе: решётка датчика

Загрузка 3D-сцены…

a = 65539, m = 2³¹. В 2D выглядит прилично — его провал видно только в 3D (тройки лягут на 15 плоскостей).Берём подряд тройки (y₃ₖ, y₃ₖ₊₁, y₃ₖ₊₂) и кидаем точками в единичный куб. Крути куб мышью: у RANDU поймаешь ракурс, где точки собираются в несколько плоскостей; у хорошего датчика они заполняют куб ровно.

Мораль простая и злая: датчик может выглядеть чистым на одном тесте и с треском спалиться на другом. Нельзя верить генератору на слово — его надо гонять по проверкам.

Уичман–Хилл: три датчика в одной упряжке

Как сделать надёжно? Не полагаться на один станок, а запустить несколько сразу и смешать. Датчик Уичмана–Хилла (1982) гоняет три мультипликативных датчика параллельно, с такими параметрами:

d1=30269,m1=171;d2=30307,m2=172;d3=30323,m3=170.\begin{aligned} d_1 = 30269, &\quad m_1 = 171; \\ d_2 = 30307, &\quad m_2 = 172; \\ d_3 = 30323, &\quad m_3 = 170. \end{aligned}

На nn-м шаге каждый выдаёт своё yn′,yn′′,yn′′′y'_n, y''_n, y'''_n, а итог — дробная часть их суммы: yn={ yn′+yn′′+yn′′′ }y_n = \{\,y'_n + y''_n + y'''_n\,\} (фигурные скобки — «взять только то, что после запятой»).

Выхлоп: период такого комбо — около 3⋅10133 \cdot 10^{13}. Это на порядки длиннее периода одиночного датчика Фишмана–Мура (231−2≈2⋅1092^{31} - 2 \approx 2 \cdot 10^9), да ещё и на компьютере считается в несколько раз быстрее. Трое по понятиям держат район куда дольше, чем один.

Что в сухом остатке

Математический датчик — это короткий рекуррентный алгоритм: задал старт (seed) — получил воспроизводимую цепочку псевдослучайных чисел, быстро и без тетрадей. Но:

  • старт полностью определяет серию (это и плюс — воспроизводимость, и зона риска — кривой старт губит всё);
  • у любого датчика есть период, после которого он идёт на повтор;
  • и, как показал RANDU, датчик может таить закономерность, незаметную на простых тестах.

Поэтому генератору нельзя верить на глаз. А чтобы вообще понять, что значит «настоящая случайность» и почему короткий алгоритм в принципе не может её выдать, — переходим к следующему параграфу, про сложность.

Проверь себя

Тест

Почему провал датчика RANDU не видно на диаграмме пар (yₙ, yₙ₊₁), но видно на тройках в 3D?
Обновлено