В предыдущих частях работы (Часть 1, Часть 2) мы разобрали, что такое линейный конгруэнтный метод (ЛКМ), и как на его основе работает функция Rnd(), вшитая в скриптовый язык VBA, созданный Microsoft. Именно эта функция и "ответственна" за генерацию псевдослучайных чисел. Мы поняли, как ведет себя функция, если в качестве ее аргумента ввести число меньшее либо равное 0. Также мы выяснили, что эта функция работает с мнимым и реальными значениями своих аргументов, также мы поняли, как соотносятся некоторые мнимые значения аргумента функции с их реальными значениями.

В третьей части исследования речь пойдет в основном о том, каким образом функция Rnd() взаимодействует с аргументами в виде дробных чисел,  а также о том, как ведет себя функция, при вводе в качестве ее аргумента больших (по модулю) чисел. Как оказалось – обе эти  темы взаимосвязаны. Итак – поехали!

Загадка числа 13421962

Прошлую часть работы я закончил на том, что при вводе в качестве аргумента числа (-0.1) функция Rnd() выдаст значение rnd, соответствующее начальному значению (x0), равному 13421962: 

О том, что такое мнимые и реальные значения аргументов функции Rnd(), а также о том, как найти реальное x0 при заданном мнимом, подробно было рассказано во второй части. Здесь же я кратко скажу, что при работе функция оперирует двумя значениями x0 - мнимым и реальным.

Мнимое значение x0 – это то значение, которое вводится в качестве аргумента функции (в данном случае -0.1), реальное значение x0 – это то значение, с которым работает функция для расчета остатка (x1), и, соответственно, для расчета значения rnd. В данном случае реальное значение x0 будет равно 13421962 (CCCD8A16), а вычисленный остаток (x1) - 5785501 (о том, как находится остаток рассказано в первой части работы). Окончательное значение rnd, которое видит пользователь, в языке VBA рассчитывается как:

rnd = \frac{x_1}{16777216}

Итак, стало ясно: передо мной появился новый вызов, который поставил передо мной задачу: почему -0.1, введенное в качестве аргумента функции, дает реальное x0, равное 13421962?   

Для начала я решил попробовать «скормить» функции различные значения аргументов. Например, -0.9:

На тот момент это мало чем могло помочь… Но тогда же у меня и появилась некоторая догадка: что если попробовать ввести в качестве аргумента отрицательные значения степени 2?

Как оказалось, такое решение имело смысл, и при этом, весьма и весьма немалый:

В итоге наметилась вполне понятная закономерность, которую можно представить в следующей таблице:

Значения реальных x0 для нечетных отрицательных значений степеней 2.
Значения реальных x0 для нечетных отрицательных значений степеней 2.

Дробные значения, равные четной степени двойки, имеют шесть 16-ричных разрядов, старшие четыре из которых равны 8000:

Значения реальных x0 для четных отрицательных значений степеней 2.
Значения реальных x0 для четных отрицательных значений степеней 2.

И тут у меня появилось понимание, почему мы получаем реальное x0, равное 8000BF16, если мы берем в качестве аргумента функции число -1. Все встало на свои места – ведь -1 можно представить как -(20). А это четная степень двойки! Значит, представленную выше таблицу можно изобразить и так:

Значения реальных x0 для четных значений степеней 2.
Значения реальных x0 для четных значений степеней 2.

Таким образом, число -1 становится не точкой отсчета, а всего лишь одним из многих чисел в ряду мнимых значений x0. Но где находится начало этого ряда? Как уже было выяснено во второй части, число 0 стоит особняком и его вполне спокойно можно вынести за скобки данного вопроса. Где заканчивается этот ряд – мы уже знаем. Это самое большое число (по модулю), которое можно представить по правилам представления 32-разрядных чисел стандарта IEEE-754.

Начало начал

В физике существуют понятия классической и квантовой механики. Физические законы, применимые в классической механике, могут не работать в квантовой, и наоборот – законы, применимые в квантовой механике, не совсем релевантны классической механике.

Работая с числами, мы также можем сталкиваться с подобным явлением. Пока речь идет об относительно крупных величинах, ЛКМ работает вполне предсказуемо, но как только речь заходит о малых величинах, этот алгоритм может выдавать, на первый взгляд, неожиданные значения.

Попробуем спуститься вниз и продолжить «вниз» таблицу для отрицательных степеней двойки, берущихся в качестве аргумента функции Rnd(). Получим, что до 2-126 функция будет вести себя стандартно:

Далее мы могли бы спрогнозировать, что степень -127 даст реальное x0 равное 128 (8016). Но нет – степень -127 даст значение x0 равное 40008016. Степень -128 – значение 20008016. В итоге получим следующее:

Итак, мы видим, что минимальное число,отличное от нуля, которому VBA Excel присваивает реальное x0 – это -2-149. Это и есть тот самый квант, та самая альфа, начало начал, которое мы и искали! Значение 2-150 VBA уже считает достаточно близким к нулю, и соответственно, дает для него реальное x0 такое же, какое программа дает и для нуля:

 Может появиться вопрос: как можно вводить в качестве аргумента столь малые значения? Очень просто: при помощи символа степени (^). То есть, чтобы ввести в качестве аргумента функции число 2-149, нам надо набрать следующее:

round = Rnd(-(2 ^ -149))

В итоге -2-149 даст значение rnd, соответствующее реальному значению x0, равному 129. Я думаю, что многие уже догадались, но все-таки для формальности нужно сказать, что -2-149 – это самое малое отрицательное число (по модулю), которое можно получить, используя 32-битное представление чисел по стандарту IEEE-754 (8000000116):

Число 2-149 в 32-битном представлении.
Число 2-149 в 32-битном представлении.

Более подробно формат представления IEEE-754 был разобран во второй части. Но, напомню, что розовый блок – это блок знака, в котором содержится число 1, что означает, что мы имеем дело с отрицательным числом. Лиловый блок – блок экспоненты. В данном случае значение в лиловом блоке равно 0, но мы должны вычесть из него 127, и в итоге получим значение -127. Соответственно, самый младший разряд зеленого блока мантиссы соответствует степени 2-149, самый старший – 2-126

Небольшой оффтоп: интересно, что число (8000000016) в формате IEEE-754 будет соответствовать т. н. отрицательному нулю. Таким образом, в указанном стандарте ноль может быть представлен в двух видах: в отрицательном и положительном:

Отрицательный 0.
Отрицательный 0.
Положительный 0.
Положительный 0.

Не совсем понятно, почему разработчики решили назначить для начальной точки именно число 129, а не 0, или, хотя бы единицу. Но что есть, то есть – минимальное число, воспринимаемое VBA, соответствует реальному значению x0 129. Таков выбор разработчиков и с этим приходится жить.

Квантовые вычисления

Итак, согласно приведенной выше таблице, мы видим, что число -2-147 соответствует реальному x0, равному 132 (8416). А число -2-148 – реальному числу 130 (8216). А есть ли такое число, которое соответствует реальному 131 (8116)?

Для наглядности изобразим все эти числа на числовой оси:

Будем считать, что число 2-149 является одним шагом, который для большего пафоса можно назвать квантом. Понятно, что расстояние от 0 до -2-149 равняется одному кванту. Далее:

2-148 = 2∙2-149 = 2-149 + 2-149

Следовательно, расстояние от -2-149 до -2-148 также равняется одному кванту. Далее: 

2-147 = 2∙2-148 = 4∙2-149  = 2-149 + 2-149 + 2-149 + 2-149 = 2-148 + 2-149 + 2-149

Отсюда делаем вывод, что расстояние от -2-148 до -2-147 будет равняться двум квантам.  

Отсюда напрашивается довольной простой вывод – реальному числу 131 будет соответствовать число, находящееся посередине между -2-147 и -2-148. Исходя из всех вышеприведенных данных, понятно, что между двумя этими числами находится число -2-148 – 2-149. Вот его-то мы и введем в качестве аргумента функции Rnd():

Rnd(-(2 ^ -148 + 2 ^ -149))
Число -2-148 - 2-149 соответствует реальному x0, равному 131.
Число -2-148 - 2-149 соответствует реальному x0, равному 131.

Наша гипотеза полностью подтвердилась! В итоге мы можем представить числовой интервал от -2-147 до 0 в следующем виде:

Реальное x0 задается количеством квантов.
Реальное x0 задается количеством квантов.

Какой вывод из следует всего этого? Мы знаем что числу -2-126 соответствует реальное x0, равное 8388736 (80008016). А числу -2-149 – реальное число 129 (8116). Соответственно, можно предположить, что интервал от -2-126 до -2-149 разделен на 8388607 кванта (8388736 - 129).

Проверим это утверждение. Число -2-126 + 2-149, согласно этой логике, должно соответствовать реальному числу, на единицу меньшему, чем реальное число, соответствующее аргументу 2-126 - это число 8388735 (80007F16). Итак, вводим следующий аргумент:

Rnd(-(2 ^ -126 - 2 ^ -149))

И получаем:

Все сходится! А что относительно числа -2-126 - 2-149? Проверяем:

Следовательно, можно сделать вывод, что квант, равный 2-149, будет присутствовать и на интервале от 2-126 до 2-125

Следим за руками

А теперь я всех призываю внимательно следить за манипуляциями с числами! Мы знаем, что числу -2-125 соответствует реальное число 129 (8116). Соответственно, мы могли бы спрогнозировать, что числу -2-125 + 2-149 будет соответствовать число 128 (8016). Проверяем и получаем следующую картину:

Вместо числа 128 мы получаем 127...
Вместо числа 128 мы получаем 127...

Упс! Число 128 куда-то испарилось… Может быть, мы пропустили какую-то «ступень», которой соответствует то самое неуловимое число 128? Возможно, чтобы найти эту ступень, мы должны прибавить к -2-125 не целый квант, а только его половину (2-150)? Но, как мы уже знаем, число 2-150 в VBA соответствует нулю, поэтому если мы введем в качестве аргумента число -2-125 + 2-150, то мы получим картину, соответствующую аргументу 2-125 без каких-либо дополнительных слагаемых:

2-150 не дает никаких изменений для реального x0.
2-150 не дает никаких изменений для реального x0.

Далее все идет без изменений и число -2-125 + 2-148 дает реальное x0, равное 126 (7E16).

Числа 128 (8016) найти пока что не удалось.
Числа 128 (8016) найти пока что не удалось.

Число 128 пока что является для нас весьма трудноуловимым – своеобразным цифровым нейтрино, скрывающимся в потоке других числовых частиц, которые генерирует разобранный нами уже, фактически, на атомы алгоритм ЛКМ.

Впрочем, в отсутствии этого числа есть и свои причины. Как мы уже стало понятно, на интервале от -2-149 до -2-126 имеется 8388607 кванта. А сколько квантов имеется по другую сторону от -2-126 – на отрезке от 2-126 до -2-125 + 2-149?

Во второй части исследования мы уже выяснили, что алгоритм даст одинаковые значения для чисел, сравнимых по модулю с числом 16277216 (100000016). Итак:

7F16 ≡ 100007F16 (mod 100000016)  

То есть, для алгоритма нет никакой разницы между числами 7F16 и 100007F16, и если взять два этих числа в качестве реального x0, на выходе функции мы получим одно и то же число (0,407612).

100007F16  = 1677734310, 80008016 = 838873610.

Таким образом, на исследуемом нами интервале мы будем иметь 8388607 квантов (16777343 - 8388736). Ровно столько же квантов мы имеем и на противоположном интервале. Итак, мы получили симметричную картину – по обе стороны от числа -2-126 находится ровно по 8388607 квантов. А кванты с номером 8388608 являются своеобразными переходами, если так можно можно выразиться, на новые энергетические уровни.

Теперь нам известно, как ведет себя функция Rnd() на интервале от 0 до 2-125.
Теперь нам известно, как ведет себя функция Rnd() на интервале от 0 до 2-125.

Выходим на новый уровень

Итак, мы разобрались с тем, что происходит справа от числа 2-125. Теперь нужно понять, что происходит слева от этого числа. 

Для этого протестируем некоторые значения мнимых аргументов функции Rnd() и получим следующую картину:

В итоге делаем вывод, что в интервале от -2-125 до -2-124 мы будем иметь дело не с найденным квантом, а с шагом, равным двум квантам (2-148).

Для удобства условимся, что квантом мы будем называть самый малый возможный шаг - в VBA это 2-149. А шагом мы будем называть любой шаг, размер которого будет больше кванта.

Соответственно, на участке от -2-124 до 2-123 шаг снова увеличится в два раза и уже будет составлять уже 2-147 (4 кванта).

В отличие от предыдущего интервала картина уже не будет симметричной – в правой части мы получим 8388608 шагов (8388737 – 129). А в левой – 8388607 шагов (16777344 - 8388737). Что интересно, мы смогли поймать «нейтрино» предыдущего интервала – числу 128 (8016) соответствует точка, являющая правой границей для шага перехода к следующему числовому интервалу. Мнимое значение x0 в этом месте равно -2-123 + 2-147. В то же время в левой части интервала исчезло число 129 (8116).

Поведение функции Rnd() на интервале от 2-125 до 2-123.
Поведение функции Rnd() на интервале от 2-125 до 2-123.

Та же самая закономерность будет наблюдаться и далее для любой нечетной степени двойки, большей или равной -125. Итак, если n – нечетное число, n ≥ -125, q – размер шага, то мы получим следующее:

Универсальная формула поведения функции Rnd() на любом доступном интервале чисел.
Универсальная формула поведения функции Rnd() на любом доступном интервале чисел.

Вычислить, какое реальное значение x0 соответствует каждой степени -2 не составит особого труда. Таблицы соответствия для некоторых значений степеней также были приведены во второй части данной работы.

Возвращаясь к -0.1

Теперь у нас имеется вся теоретическая база для того, чтобы с успехом решить заявленную в самом начале этого текста загадку числа 13421962. Итак, -0,1 лежит между числами-1/8 (-(2-3)) и -1/16 (–(2-4)). Соответственно, n для этого числа будет равен -5, а показатель степени размера кванта на данном промежутке составит n-22 = -5 -22 = -27.

Мы уже знаем, что аргументу -1/16 соответствует реальное значение 8388797 (8000BD16). Итак, для того, чтобы найти реальное число, соответствующее аргументу -0.1, нам нужно узнать, сколько нужно сделать шагов размером 2-27 от числа -1/16 до числа -1/10. Затем это число мы прибавляем к значению 8388797 и, таким образом, получаем реальное значение, соответствующее мнимому значению аргумента -0.1.

Число N равняется количеству шагов размером 2-27 от -1/16 до -1/8.
Число N равняется количеству шагов размером 2-27 от -1/16 до -1/8.

На самом деле мы уже знаем количество шагов (N) – подсказка у нас уже есть. Мы знаем, что аргументу -0.1 соответствует реальное x0, равное 13421962. Соответственно, N = 13421962 - 8388797 = 5033165.

Проверяем нашу гипотезу. Можно вычислить N, как говорится в лоб. Расстояние  от  -0.1 до -1/16  (S) равно 3/80.

-0.1 = -\frac{1}{16} - S \Rightarrow S = 0.1 - \frac{1}{16} = \frac{8}{80} - \frac{5}{80} = \frac{3}{80}

Теперь мы можем вычислить N:

^N = \frac{S}{2^{-27}} = \frac{3}{80} : \frac{1}{2^{27}} = \frac{3\cdot2^{27}}{5\cdot16} =  \frac{3\cdot2^{24}}{5} = 5033164,8

Понятно, что число 5033164,8 округляется до 5033165, что дает нам N, совпадающее с ранее полученными данными. В принципе, гипотеза является доказанной, но доказанной, если так можно выразиться, постфактум.

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

Итак, в данном случае точность у нас равняется 2-27. Это означает, что нам необходимо представить число -0.1 в двоичном виде с точностью до 27 знака после запятой. Сделать это можно в любом калькуляторе, который переводит десятичные числа в двоичные числа. Переводим десятичную дробь -0.1 в двоичную:

-0.110 = -0.0001 1001 1001 1001 1001 1001 1001 1001 …2 

Мы видим, что в 27 разряде после запятой в двоичной дроби имеется 0. Но в 28 разряде имеется единица. Соответственно, по правилам двоичного округления при округлении до 27 разрядов последний разряд числа у нас будет равняться 1.

Следовательно:      

-0.110 ≈ -0.0001 1001 1001 1001 1001 1001 1012 

В итоге получим, что:

-0.110 ≈ -(2-4) – 2-5 – 2-8 – 2-9 – 2-12 – 2-13 – 2-16 – 2-17 – 2-20 – 2-21 – 2-24 – 2-25 – 2-27

Теперь нам следует убрать из этой суммы первое слагаемое и выяснить, сколько в получившемся значении содержится шагов размеров 2-27. Надеюсь понятно, почему нам не следует трогать число -(2-4) – оно является своеобразной вехой, от которого идет отсчет. Мы знаем, что числу -(2-4) соответствует реальное реальное x0, равное 8388797 и для того, чтобы получить реальное x0 для -0.1 нам нужно будет прибавить к числу 8388797 искомое число N.

Итак, 2-5  = 222 ∙ 2-27, 2-8  = 219 ∙ 2-27 и т. д. В итоге имеем:

-0.110 ≈ -(2-4) – 2-27 ∙ (222 + 219 + 218 + 215 + 214 + 211 + 210 + 27 + 26 + 23 + 22 + 1) ≈ -(2-4) – 2-27 ∙ ∙ (4194304 + 524288 + 262144 + 32768 +16384+ 2048 + 1024 + 128 + 64 + 8 + 4 + 1) ≈ ≈ -(2-4) – 2-27 ∙ 5033165

Вау!!! Хотя, такой результат и был ожидаем, но все же, на мой взгляд, он достоин восхищения. Мы вместе прошли все этапы очень долгого пути, чтобы, наконец, с полным правом сказать: мы знаем о функции Rnd() почти все…    

Небольшая формальность: введем в качестве аргумента функции Rnd() следующие числа:

-0.1
a = -(2 ^ -4 + 2 ^ -5 + 2 ^ -8 + 2 ^ -9 + 2 ^ -12 + 2 ^ -13 + 2 ^ -16 + 2 ^ -17 + 2 ^ -20 + 2 ^ -21 + 2 ^ -24 + 2 ^ -25 + 2 ^ -27)

Получаем:

Тайна числа 13421962 раскрыта!
Тайна числа 13421962 раскрыта!

Итак, тайна числа 13421962 полностью раскрыта! Для любителей поломать себе мозги предлагаю по этому же алгоритму «разложить» любую другую десятичную дробь и сравнить полученные результаты с теми, которые выдаст алгоритм. Далеко ходить не надо, можно взять уже упомянутое число -0.9:

Есть желающие разгадать тайну числа 6711077?
Есть желающие разгадать тайну числа 6711077?

Еще не конец

Внимательные читатели должны были заметить, что выше я сказал о том, что теперь мы знаем о функции Rnd() почти всё. Но, наверное, если бы все было так просто, как кажется на первый взгляд, вряд ли мы могли говорить о том, что имеем дело с продуктом корпорации из списка Big Five. А закладывать "сюрпризы" в свои детища они весьма и весьма любят - вот и здесь, как оказалось, не обошлось без такого сюрприза. Итак, поехали!

Понятно, что размер шага с каждым новым интервалом увеличивается в два раза, пока в один прекрасный момент не станет равным двум. Это произойдет на интервале от -224 (-16777216) до -225 (-33554432).

Итак, по логике, если числу -16777216 соответствует реальное x0, равное 8388811 (8000CB16), то мнимому аргументу -16777218 должно соответствовать реальное число 8388812 (8000CC16), поскольку шаг на этом интервале у нас стал равным 2. Проверяем:  

  Да, все сходится. А что насчет числа -16777217? Выше уже говорилось, что любое число на интервале, меньшее шага этого интервала, VBA считает нулем. Соответственно, и число 1 на указанном интервале должно уже считаться нулем. Итак, -16777216 – 1 должно давать реальное x0, соответствующее числу -16777216, что мы и видим:

   Казалось бы – вот и всё! Какие еще могут быть сюрпризы? По этой же логике число -16777219 должно давать реальное x0 8388812:  

Упс...
Упс...

И здесь нас ждет очередной «упс». Почему-то число -16777219 выдает реальное x0 8388813, словно у нас неожиданно уменьшился шаг… Что же, надо идти дальше, в итоге получаем следующую картину:

Оказывается, после первых двух чисел интервала, которые будут соответствовать одному и тому же реальному x0, мы будем иметь дело с чередованием серий чисел длиной 1 и 3 соответственно. При этом каждое число серии будет соответствовать одному и тому же реальному x0.

А как обстоит дело в конце интервала?

Ситуация в конце интервала от -(2-24) до -(2-25)
Ситуация в конце интервала от -(2-24) до -(2-25)

Как видно, последнее целое число интервала соответствует тому же реальному x0, которому соответствует и первое число следующего интервала (сделаем допущение, что число -225 в интервал не входит). Далее все стандартно – чередуются серии из одного и трех целых чисел. Таким образом, мы можем записать следующую формулу для интервала от -224 до -225:

-225 = -224 – 2 – 1 – 3 – 1 – 3 – 1 …– 3 – 1 – 3 – 1 – 1

  Не вдаваясь в подробности, запишем формулы для нескольких следующих промежутков (получены опытным путем):

-226 = -225 – 3 – 3 – 5 – 3 – 5 – 3 … – 5 – 3 – 5 – 3 – 2   

-227 = -226 – 5 – 7 – 9 – 7 – 9 – 7… – 9 – 7 – 9 – 7 – 4

-228 = -227 – 9 – 15 – 17 – 15 – 17 – 15 … – 17 – 15 – 17 – 15 – 8

Понятно, что в каждой из формул каждое слагаемое (кроме первого) указывает на серию из целых чисел, дающих одинаковые x0.

Как можно получить формулу для следующего интервала (от -228 до -229)? Думаю, внимательные читатели уже могли заметить закономерность.

Понятно, что первым числом формулы будет -228. Второе число мы получим, взяв четвертое число из формулы для предыдущего интервала – оно же будет равно третьему с краю числу из этой же формулы. Вот такая рекурсия. В данном случае – это число -17. Теперь нам нужно получить числа для повторяющихся серий. В данном случае шаг q у нас будет равен 32. Соответственно, сумма чисел в сериях, как видно из предыдущих формул, должна будет равняться -2q. Обозначим эти числа как a и b:

a + b = -2q

Из предыдущих формул мы видим, что:

a = - (q-1)\\b = - (q +1)

Таким образом, в данном случае a будет равно -31, b – -33. Последнее число суммы будет равно удвоенному последнему числу формулы для предыдущего интервала.    

Итак, теперь у нас есть формула для еще одного интервала:

-229 = -228 – 17 – 31 – 33 – 31 – 33 – 31 … – 33 – 31 – 33 – 31 – 16

По такой же схеме мы сможем вычислить формулы для любого интервала, как теоретического, так и практически доступного для работы функции Rnd() языка VBA.

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

Комментарии (0)