[Перевод] Решение Fizzbuzz при помощи теоремы Эйлера
FizzBuzz — это известная задачка на программирование, которую обычно дают в технической части собеседований. Она формулируется примерно так:
Напишите функцию, выводящую список целых чисел от 1 до 100, но вместо каждого числа, кратного 3, она должна выводить «Fizz», а вместо каждого числа, кратного 5, выводить «Buzz». Вместо чисел, кратных и 3, 5, программа должна выводить «FizzBuzz»; все остальные числа должны выводиться без изменений.
Можно написать функцию, вообще не использующую условную логику и вместо этого разделяющую целые числа на 4 возможные категории (обычное решение оставим в качестве упражнения заинтересованному читателю):
- Имеющие делитель 3, но не 5
- Имеющие делитель 5, но не 3
- Имеющие делитель и 3, и 5
- Не имеющие делитель 3 и 5
Нам нужна функция, которая будет возвращать:
Рассмотрим реализацию такой функции на Python:
[(lambda n: { 1: n, 6: "Fizz", 10: "Buzz", 0: "FizzBuzz" }[n**4%15])(n+1) for n in range(100)]
Та же функция на Ruby:
(1..100).map{|n| {1 => n, 6 => "Fizz", 10 => "Buzz", 0 => "FizzBuzz"}[n**4%15] }
Как мы и ожидали, каждая из этих функций возвращает список целых чисел от 1 до 100 с подставленными в нужные места «Fizz», «Buzz» и «FizzBuzz».
Но почему? Откуда взялись постоянные значения 0, 6, 10 и 1? Почему возвращает 6 для чисел, кратных 3, но не 5, 10 для чисел, кратных 5, но не 3, 0 для чисел, кратных 5 и 3 и 1 во всех остальных случаях? И самое важное — справедливо ли это для любого , которое мы выберем?
I. n^4 mod 15 решает FizzBuzz
Доказательство таково:
Первый случай тривиален; если n делится и на 3, и на 5, то после возведения n в любую степень возвращаемый результат всегда будет равен 0, та как любое целое число с делителями 3 и 5 будет также кратным 15 .
Во втором случае у нас есть 3 как делитель некой константы c:
или
для каждой константы c > 0, если c является взаимно простым с 5. (Если c не является взаимно простым с 5, то у нас возник первый случай, который вернёт 0.)
Для начала предположим, что . Это даёт или , что вернёт 6: , с остатком 6 от 81.
Предположим, что c является целым > 1. Применим степень к каждому множителю и воспользуемся равенством :
Давайте рассмотрим отдельно выражение . Из теоремы Эйлера в теории чисел мы знаем, что поскольку является взаимно простым с 5, то равно 1; если имеет остаток 1, то это должен быть случай, когда ровно делится на 5; то есть, вне зависимости от значения , если оно является взаимно простым с 5, то имеет делитель 5. Другой делитель не важен, поэтому давайте перепишем как .
Мы можем переписать это во вторую строку благодаря следующему свойству модулярных выражений: . В третьей строке выражение имеет делители и 3, и 5, поэтому , что даёт нам , то есть 6.
То есть для любого , являющегося взаимно простым с 5 и > 1, выражение вернёт 6.
Благодаря тому же процессу мы выясним, что всегда возвращает 10; при мы имеем , что равно 10. Для
Теорема Эйлера говорит нам, что если является взаимно простым с 3, то ;, но у нас есть . Однако — это , а поскольку , то также равно 1.
Опять же, если , то нацело делится на 3 и имеет делитель 3. Это снова сокращает второй член до 0, оставляя , что снова равно 10.
У нас остаётся случай, в котором есть некое число , являющееся взаимно простым с 3 и 5. Если равно 1, то получаем , то есть 1.
Если > 1, мы снова можем записать это как . Так как мы знаем, что если является взаимно простым с 3 и 5, и , то имеет делители 3 и 5.
Поэтому снова:
То есть всегда будет возвращать 0, если и 3, и 5 являются делителями , возвращать 6, если делителем является 3, но не 5, возвращать 10, если делителем является 5, но не 3, и 1, если является взаимно простым и с 3, и с 5, а функция вида
->(n){ {1 => n, 6 => "Fizz", 10 => "Buzz", 0 => "FizzBuzz"}[n**4%15] }
всегда будет возвращать правильный результат FizzBuzz для каждого (до того, как не переполнит тип integer, используемый в компьютере).II. Решение любой задачи категории FizzBuzz
Схожую функцию можно создать для любой похожей задачи, которые я буду называть задачами категории FizzBuzz. (Задачи категории FizzBuzz: например, выводить «Foo» вместо чисел, кратных 2, «Bar» вместо чисел, кратных трём, «Baz» вместо чисел, кратных 5 и выполнить конкатенацию каждого слова для чисел, имеющих больше одного из делителей 2,3 и 5). Выбираемые в качестве делителей числа не обязаны быть простыми, но если одно из них имеет в качестве делителя другое, то некоторые группы не существуют. Например, если мы решили изменить FizzBuzz так, чтобы кратные 2 возвращали «Fizz», а кратные 4 возвращали «Buzz» (вместо традиционных 3 и 5), то увидим «Fizz» для каждого кратного 2, «FizzBuzz» для кратного 2 и 4, но никогда не увидим «Buzz», потому что не существует числа, кратного 4, являющегося взаимно простым с 2.
Представленное ниже обобщённое решение не такое уж и общее, как я сказал: существует множество пар чисел, для которого оно не сработает; поэтому делители нужно выбирать гораздо тщательнее. Оно сработает для уникальных простых чисел, но не всегда хорошо для составных чисел. Хочу поблагодарить автора этого очень подробного поста, объясняющего подробности того, почему общая формула не будет работать для составных чисел.
Написанное выше уравнение для решения FizzBuzz можно также записать в виде
где — это пси-функция Эйлера, а — наименьшее общее кратное.
В общем виде для любого множества из делителей , для которого нам нужна функция типа FizzBuzz, можно использовать формулу:
Для функции типа FizzBuzz, возвращающей «Foo» вместо чисел, кратных 7, и «Bar» вместо чисел, кратных 11, найдём:
равно 6, а равно 10, и (наименьшее общее кратное) для 6 и 10 равно 30. То есть уравнение примет вид:
Реализация на Python:
[(lambda n: { 1: n, 7**30%77: "Foo", 11**30%77: "Bar", 0: "FooBar" }[n**30%77])(n+1) for n in range(100)]
Это даст нам ожидаемый результат:
[1, 2, 3, 4, 5, 6, 'Foo', 8, 9, 10, 'Bar', 12, 13, 'Foo', 15, 16, 17, 18, 19, 20, 'Foo', 'Bar', 23, 24, 25, 26, 27, 'Foo', 29, 30, 31, 32, 'Bar', 34, 'Foo', 36, 37, 38, 39, 40, 41, 'Foo', 43, 'Bar', 45, 46, 47, 48, 'Foo', 50, 51, 52, 53, 54, 'Bar', 'Foo', 57, 58, 59, 60, 61, 62, 'Foo', 64, 65, 'Bar', 67, 68, 69, 'Foo', 71, 72, 73, 74, 75, 76, 'FooBar', 78, 79, 80, 81, 82, 83, 'Foo', 85, 86, 87, 'Bar', 89, 90, 'Foo', 92, 93, 94, 95, 96, 97, 'Foo', 'Bar', 100]
Вывод
Итак,
$$display$$a^{LCM (ϕ (n_1), ϕ (n_2),…ϕ (n_k))} \equiv \begin{cases} 0\pmod {\prod_{i=1}^k n_i}, & \text{if $n$ is divisible by every $n_1, n_2,…n_k$} \newline r_1, r_2,…r_k\pmod {\prod_{i=1}^k n_i}, & \text{a distinct result for every other combination of factors in $n_1, n_2,…n_k$} \newline 1\pmod {\prod_{i=1}^k n_i}, & \text{if $n$ is coprime to every $n_1, n_2,…n_k$} \end{cases}$$display$$
… если числа являются уникальными простыми числами. Снова благодарю автора этого поста за столь наглядную иллюстрацию этого.
Я не делаю никаких предположений о том, что существует какое-то иное практическое применение этой формулы, кроме как для решения задач категории FizzBuzz с выбранными в качестве кандидатов произвольными числами.