[Перевод] Делимые факториалы
Недавно я был совершенно сбит с толку этим твитом «Библиотеки Ферма»:
«Вот что получится, если в факториале не умножать, а делить.»
Когда я увидел его, мне пришлось бросить свои дела, схватить блокнот и проверить форулу. Результат в черновом виде казался логичным. Так как мультипликативная версия при увеличении стремится к бесконечности, то «делительная» версия должна стремиться к нулю. И ведёт себя именно так; полиномиальная функция растёт медленнее, чем степенная функция для достаточно больших :
Но почему результат деления принимает именно вид ? Откуда берётся ?
Чтобы ответить на этот вопрос, мне пришлось разбередить старую травму, связанную с изучением деления дробей, но я справился с болью. Двигаясь по формуле из твита слева направо, мы сначала получаем . Затем, поделив эту величину на , получаем
Продолжая таким образом, мы в результате приходим к:
Чтобы прийти к показанному в твите результату , мы просто умножим числитель и знаменатель на . (Хотя на мой вкус, выражение более понятно.)
Я официально признанный фанат факториалов. Оставьте при себе свои последовательности Фибоначчи; вот моя любимая функция. Каждый раз, когда я изучаю новый язык программирования, моим первым упражнением становится написание нескольких процедур для вычисления факториалов. За многие годы я придумал несколько вариаций этой темы, например, замену в определении на (что даёт нам треугольные числа). Но кажется, что раньше я никогда не задумывался о замене на . Получается странно. Так как умножение коммутативно и ассоциативно, мы можем определить просто как произведение всех целых чисел от до , не беспокоясь о порядке операций. Но при делении порядок игнорировать не получится. В общем случае, и .
В твите «Библиотеки Ферма» делители поставлены в порядке по убыванию: . Очевиднее всего будет заменить это на порядок по возрастанию: . Что произойдёт, если мы зададим факториал деления как ? Ещё один возврат к школьному алгоритму деления дробей даёт нам простой ответ:
Другими словами, когда мы многократно выполняем деление, выполняя подсчёт от до , окончательный результат будет равен величине, обратной . (Мне хотелось бы поставить в конце этого предложения восклицательный знак, но увы!) Если вы ищете канонический ответ на вопрос «Что мы получим при делении вместо умножения в ?», то я бы заявил, что — лучший кандидат, чем . Почему бы нам не принять симметрию между и обратной ему величиной?
Разумеется, есть множество других способов размещения n целочисленных значений во множестве . Но сколько именно? Как оказалось, ровно ! Поэтому, может показаться, что есть уникальных способов задания делительной функции . Однако, изучение ответов двух показанных выше перестановок даёт нам понять, что здесь работает более простой паттерн. Какой бы элемент последовательности не появился первым, он оказывается в числителе большой дроби, а знаменателем оказывается произведение всех других элементов. Поэтому в итоге остаётся всего различных результатов (если предположить, что мы всегда выполняем операции деления строго слева направо). Для любого целочисленного в интервале от до , поставив в начало очереди, мы создаём делительное , равное , поделённому на все другие коэффициенты. Можно записать это следующим образом:
И таким образом мы решили небольшую загадку о том, как в этом твите превратилось в .
Стоит заметить, что все эти функции сходятся к нулю при стремлении к бесконечности. С асимптотической точки зрения, идентичны.
Та-да! Миссия выполнена. Задача решена. Дело сделано. Теперь мы знаем всё, что нам нужно, о делительных факториалах, верно?
Ну, возможно, есть ещё один вопрос. Что скажет компьютер? Если взять наш любимый алгоритм факториала, и сделать то, что предлагается в твите, заменив все вхождения оператора (или *
) на /
, то что случится? Какие из вариантов делительного выдаст нам программа?
Вот мой любимый алгоритм для вычисления факториалов в виде программы на Julia:
function mul!(n)
if n == 1
return 1
else
return n * mul!(n - 1)
end
end
Этот алгоритм познакомил целые поколения нердов с концепцией рекурсии. В текстовом виде он гласит: если равно , то равно . В противном случае нужно вычислить функцию , а затем умножить результат на .
Вы можете спросить, что произойдёт, если будет равным нулю или отрицательным. Спросить вы можете, но лучше не надо. Для наших текущих целей .
Начав с любого положительного , последовательность рекурсивных вызовов рано или поздно опустится к .
Функцию можно записать более лаконично с помощью однострочного стиля определений Julia:
mul!(n) = n == 1 ? 1 : n * mul!(n - 1)
Правая часть оператора присваивания — это условное выражение, или тернарный оператор, имеющий вид a ? b : c
. Здесь a
— булево условие теста, которое должно вернуть значение или true
, или false
. Если a
равно true
, то вычисляется выражение b
, а результат становится значением всего выражения. В противном случае вычисляется c
.
Просто чтобы убедиться, что я сделал всё верно, вот первые 10 факториалов, вычисленных этой программой:
[mul!(n) for n in 1:10]
10-element Array{Int64,1}:
1
2
6
24
120
720
5040
40320
362880
3628800
Теперь давайте изменим это определение и преобразуем единственное вхождение *
в /
, оставив всё остальное неизменным (за исключением названия функции).
div!(n) = n == 1 ? 1 : n / div!(n - 1)
И вот что вернёт программа, если мы запустим её для значений от до :
[div!(n) for n in 1:20]
20-element Array{Real,1}:
1
2.0
1.5
2.6666666666666665
1.875
3.2
2.1875
3.657142857142857
2.4609375
4.063492063492063
2.70703125
4.432900432900433
2.9326171875
4.773892773892774
3.14208984375
5.092152292152292
3.338470458984375
5.391690662278897
3.523941040039063
5.675463855030418
Что? Это точно не походит на схождение к нулю, как и на или . На самом деле значения так не выглядят, потому что и не собираются сходиться. Судя по показанному ниже графику, последовательность состоит из двух перемежающихся компонентов, каждый из которых, похоже, медленно растёт в сторону бесконечности, а также отклоняется от другого.
Разбираясь с тем, что же мы здесь наблюдаем, полезно будет изменить тип выходных данных функции div!
. Вместо использования оператора деления /
, который возвращает значение как число с плавающей запятой, мы можем заменить его оператором //
, возвращающим точное рациональное значение, округлённое до младшего члена.
div!(n) = n == 1 ? 1 : n // div!(n - 1)
Вот последовательность значений для n в интервале 1:20
:
20-element Array{Real,1}:
1
2//1
3//2
8//3
15//8
16//5
35//16
128//35
315//128
256//63
693//256
1024//231
3003//1024
2048//429
6435//2048
32768//6435
109395//32768
65536//12155
230945//65536
262144//46189
В списке полно любопытных паттернов. Это двойная спираль, в которой чётные и нечётные числа зигзагами перемещаются в комплементарных нитях. Чётные числа не просто чётные, все они являются степенями . Кроме того, они появляются в парах — сначала в числителе, затем в знаменателе — и их последовательность неубывающая. Но существуют пробелы; присутствуют не все степени . Нечётная нить выглядит ещё более сложной, в числах появляются и исчезают разные небольшие простые коэффициенты. (Простые числа должны быть малыми, как минимум, меньше .)
Этот результат удивил меня. Я ожидал увидеть гораздо более смирную последовательность, наподобие тех, которые я вычислял на бумаге. Все эти изломанные скачки вверх и вниз не имели никакого смысла. Как не имел смысла и общий тренд к неограниченному росту соотношения. Как мы можем постоянно делить, получая при этом всё бОльшие и бОльшие числа?
На этом этапе можете приостановить чтение и попытаться придумать собственную теорию о том, откуда взялись эти зигзагообразные числа. Если вам нужна подсказка, то у вас она есть, и очень сильная, почти спойлер: поищите последовательность числителей или последовательность знаменателей в «Онлайн-энциклопедии целочисленных последовательностей».
Вот ещё одна подсказка. Небольшое изменение в программе div!
полностью преобразует выходные данные. Просто изменим последнее выражение, заменив n // div!(n - 1)
на div!(n - 1) // n
.
div!(n) = n == 1 ? 1 : div!(n - 1) // n
Теперь результаты выглядят вот так:
10-element Array{Real,1}:
1
1//2
1//6
1//24
1//120
1//720
1//5040
1//40320
1//362880
1//3628800
Это обратная функция факториала, которую мы уже видели, ряд значений, сгенерированные при обходе слева направо по возрастающей последовательности делителей .
Неудивительно, что изменение последнего выражения в процедуре менят результат. В конце концов, мы знаем, что деление не коммутативно и не ассоциативно. Но сложно понять, почему последовательность сгенерированных исходной программой значений даёт такую странную зигзагообразную форму. Какой механизм порождает такие парные степени двойки и попеременные нечётные и чётные значения?
Я обнаружил, что объяснить происходящее в зигзагообразной последовательности проще на итеративной версии процедуры, а не на рекурсивной. (Это заявление может показаться досадным тем, кто считает рекурсивные определения более простыми, но так уж получилось.) Вот как выглядит программа:
function div!_iter(n)
q = 1
for i in 1:n
q = i // q
end
return q
end
Я заявляю, что эта процедура с циклом по функционалу идентична рекурсивной функции, в том смысле, что если div!(n)
и div!_iter(n)
возвращают результат для какого-то положительного целого n
, то он всегда будет одинаковым. Вот моё доказательство:
[div!(n) for n in 1:20] [div!_iter(n) for n in 1:20]
1 1//1
2//1 2//1
3//2 3//2
8//3 8//3
15//8 15//8
16//5 16//5
35//16 35//16
128//35 128//35
315//128 315//128
256//63 256//63
693//256 693//256
1024//231 1024//231
3003//1024 3003//1024
2048//429 2048//429
6435//2048 6435//2048
32768//6435 32768//6435
109395//32768 109395//32768
65536//12155 65536//12155
230945//65536 230945//65536
262144//46189 262144//46189
Чтобы понять процесс, порождающий эти числа, рассмотрим последовательные значения переменных и при каждом выполнении цикла. Изначально и равны ; поэтому после первого прохода цикла выражение q = i // q
даёт значение . Затем , а , то есть новое значение равно . При третьей итерации , а , что даёт нам . Если это всё ещё сбивает вас с толку, то представьте как . Важным наблюдением здесь является то, что при каждом обходе цикла получает обратное значение, становясь .
Если развернуть эти операции и посмотреть на умножения и деления, входящие в каждый элемент ряда, то возникает паттерн:
В общем виде:
Функции для нечётного и для чётного имеют собственное название! Они называются двойными факториалами, и записываются как .
Ужасная терминология, правда? Лучше бы их назвали «полуфакториалами». И если бы я этого не знал, то прочитал бы как «факториал факториала».
Двойной факториал n определяется как произведение n и всех меньших положительных целых чисел той же чётности. Таким образом, наша любопытная последовательность зигзагообразных значений — это просто .
В статье 2012 года Генри У. Гулда и Джослин Куэнтенс (увы, находящаяся за paywall) исследуются применения двойных факториалов. Они встречаются гораздо чаще, чем можно подумать. В середине 17-го века Джон Валлис вывел следующее тождество:
Ещё более странный ряд с участием куба значений двойных факториалов суммируется в . Его обнаружил не кто иной, как Сриниваса Рамануджан.
Гулд и Киэнтенс также рассматривали эквивалент двойного факториала для биномиальных коэффициентов. Стандартный биномиальный коэффициент определяется как:
Двойная версия выглядит так:
Заметьте, что наши зигзагообразные числа соответствуют этому описанию, а потому могут считаться биномиальными коэффициентами двойных факториалов. Говоря конкретнее, они являются такими числами:
Обычный бином не очень интересен; он просто равен . Но двойная версия , как мы видели, танцует более оживлённый танец. И в отличие от обычного бинома она не всегда бывает целочисленной. (Единственные целые значения — это и .)
Взгляд на зигзагообразные числа как на частное двойных факториалов объясняет довольно многие их свойства, начиная с попеременных чётных и нечётных значений. Также мы можем увидеть, почему все чётные числа в последовательности являются степенями 2. Рассмотрим пример с . Числитель этой дроби — это , получающий от множитель . Но знаменатель равен . Тройки сверху и снизу сокращаются, оставляя нам . Такие сокращения происходят в каждом из случаев. Каждый раз, когда в последовательности чётных чисел появляется нечётный множитель , он обязательно имеет вид , но к этому времени само уже должно появиться в последовательности нечётных чисел.
Является ли последовательность зигзагообразных чисел разумным ответом на вопрос: «Что произойдёт, если мы будем делить, а не умножать в ?» Или генерирующая их компьютерная программа просто оказалась ошибочным алгоритмом? По моему личному мнению, — более интуитивный ответ, зато — более интересный.
Более того, само существование зигзагообразной последовательности расширяет наши горизонты. Как сказано выше, если вы настаиваете, что алгоритм деления всегда должен по порядку проходить по списку числителей , на каждом шаге деля число слева на число справа, то имеется всего возможных результатов, и все они выглядят очень похожими. Но зигзагообразное решение обеспечивает намного более широкие возможности. Мы можем сформулировать задачу следующим образом: возьмём множество числителей , выберем его подмножество и обратим все элементы этого подмножества; теперь перемножим все числители, как обратные, так и прямые. Если обращённое подмножество пусто, то результатом будет обычный факториал . Если все числители превратились в обратные им значения, то мы получаем обратный . А если обращён каждый второй числитель, начиная с , то результатом будет элемент зигзагообразной последовательности.
Это только некоторые из множества возможных вариантов; в сумме здесь есть подмножеств из элементов. Например, можно брать обратные значения каждого числа, являющегося простым или степенью простого числа . При малых результаты скачут, но постоянно остаются меньше, чем :
Однако если бы я продолжил этот график для бОльших , он бы взлетел в стратосферу. Степени простых чисел становятся очень разреженными на числовой прямой.
Теперь я задам вопрос. Мы видели вариации факториалов, приближающиеся к нулю при стремлении к бесконечности, например . Мы видели другие вариации, растущие при увеличении безгранично, в том числе сам и зигзагообразные числа. Существуют ли какие-то разновидности процесса факториалов, сходящиеся к какой-то конечной границе, не являющейся нулём?
В первую очередь мне пришёл в голову такой алгоритм:
function greedy_balance(n)
q = 1
while n > 0
q = q > 1 ? q /= n : q *= n
n -= 1
end
return q
end
Мы циклически перебираем целые значения от вниз до , вычисляя в процессе текущее произведение/частное . На каждом шаге, если текущее значение больше , мы делим его на следующий числитель, в противном случае, выполняем умножение. Эта схема реализует своего рода управление обратной связью или поведение поиска цели. Если становится слишком большим, мы уменьшаем его; если слишком маленьким, мы увеличиваем его. Я предположил, что при стремлении к бесконечности, будет сходиться к постоянно сужающемуся интервалу значений рядом с .
Но эксперимент подкинул мне ещё один сюрприз:
Такая пилообразная волна — не совсем то, чего я ожидал. Любопытно, что кривая не симметрична около ; отклонения сверху имеют бОльшую амплитуду, чем снизу. Но это искажение больше визуальное, чем математическое. Так как является частным, расстояние от до такое же, как расстояние от до , но в линейном масштабе таким не выглядит. Исправить это можно, составив логарифмический график частного:
Теперь график симметричен, или хотя бы приблизительно таков, и центрирован относительно значения , которое является логарифмом . Но остаётся более серьёзная тайна. Пилообразная волна очень регулярна и имеет период , при этом не показывает признаков сжатия по направлению к ожидаемому ограничивающему значению . Численные значения предполагают, что при стремлении к бесконечности пики кривой сходятся к значению чуть выше , а минимумы приближаются к значению чуть ниже . (Соответствующие логарифмы по основанию примерно равны ). Мне не удалось разобраться, почему так происходит. Возможно, кто-то сможем объяснить.
Неудача с этим жадным алгоритмом не означает, что мы не сможем делительный факториал, сходящийся к .
Если мы будем работать с логарифмами числителей, то эта процедура становится случаем хорошо известной вычислительной задачи под названием «задача разбиения множества чисел». Нам даётся множество вещественных чисел, и мы должны разделить их на два множества, сумма которых равна, или как можно ближе к равенству. Это подтверждённо сложная задача, но её также называют (PDF) «простейшей сложной задачей».
Для любого мы можем обнаружить, что при использовании обратных значений какого-то другого подмножества числителей даёт нам лучшее приближение к . Для малых мы можем решить эту задачу способом перебора: просто рассмотреть все подмножеств и выбрать самое лучшее.
Я вычислил оптимальные разбиения вплоть до , когда выбирать нужно из миллиарда вариантов.
Очевидно, что график становится всё более плоским. Можно использовать тот же метод для принудительного схождения к любому другому значению в интервале от до .
И таким образом мы получаем ещё один ответ на вопрос, заданный твитом и начавший наше путешествие. Что произойдёт, если мы будем делить, а не умножать в ? Всё, что нам угодно.