Fizz Buzz — это игра с числами, которая стала неожиданно популярной в мире компьютерного программирования в качестве простой проверки базовых навыков. Правила игры просты: игроки вслух произносят по порядку числа, начиная с единицы. Если число делится на 3, игрок должен сказать вместо него «Fizz». Если число делится на 5, он должен сказать «Buzz». Если оно делится и на 3, и на 5, игрок говорит «FizzBuzz». Вот типичная программа на Python, выводящая нужную последовательность:

for n in range(1, 101):
    if n % 15 == 0:
        print('FizzBuzz')
    elif n % 3 == 0:
        print('Fizz')
    elif n % 5 == 0:
        print('Buzz')
    else:
        print(n)

А вот её вывод: fizz-buzz.txt. Можно ли усложнить эту программу? Слова «Fizz», «Buzz» и «FizzBuzz» повторяются в этой последовательности периодически. А что ещё у нас есть периодического? Тригонометрические функции! Возможно, нам удастся при помощи этих функций закодировать все четыре правила последовательности в выражении в аналитическом виде. Именно эту задачу мы и исследуем в статье, получив в конце дискретный ряд Фурье, который может получить любое целочисленное n и выбрать для печати соответствующий текст.

Определения

Прежде, чем двигаться дальше, сформулируем чёткое математическое определение последовательности Fizz Buzz. Начнём мы с нескольких функций, которые позже помогут нам определить последовательность Fizz Buzz.

Символьные функции

Мы определим множество из четырёх функций \{ s_0, s_1, s_2, s_3 \} для целочисленных n следующим образом:

\begin{align*} s_0(n) &= n, \\ s_1(n) &= \mathtt{Fizz}, \\ s_2(n) &= \mathtt{Buzz}, \\ s_3(n) &= \mathtt{FizzBuzz}. \end{align*}

Мы называем их символьными, потому что они генерируют все элементы, встречающиеся в последовательности Fizz Buzz. Символьная функция s_0 возвращает само n. Функции s_1,
s_2 и s_3 — функции-константы, всегда возвращающие слова \mathtt{Fizz}, \mathtt{Buzz} и \mathtt{FizzBuzz}, каким бы ни было значение n.

Последовательность Fizz Buzz

Теперь мы можем определить последовательность Fizz Buzz, как последовательность (s_{f(n)}(n))_{n = 1}^{\infty}, где

f(n) = \begin{cases} 1 & \text{if } 3 \mid n \text{ and } 5 \nmid n, \\ 2 & \text{if } 3 \nmid n \text{ and } 5 \mid n, \\ 3 & \text{if } 3 \mid n \text{ and } 5 \mid n, \\ 0 & \text{otherwise}. \end{cases}

Запись m \mid n означает, что целое m делит целое n, то есть n кратно m. Эквивалентно, существует такое целочисленное c, что n = cm. Аналогично, m \nmid n означает, что m не делит n, то есть n не кратно m. С учётом вышеприведённых определений мы можем разложить первые несколько членов последовательности в явном виде следующим образом:

\begin{align*} (s_{f(n)}(n))_{n = 1}^{\infty} &= (s_{f(1)}(1), \; s_{f(2)}(2), \; s_{f(3)}(3), \; s_{f(4)}(4), \; s_{f(5)}(5), \; s_{f(6)}(6), \; s_{f(7)}(7), \; \dots) \\ &= (s_0(1), \; s_0(2), \; s_1(3), \; s_0(4), s_2(5), \; s_1(6), \; s_0(7), \; \dots) \\ &= (1, \; 2, \; \mathtt{Fizz}, \; 4, \; \mathtt{Buzz}, \; \mathtt{Fizz}, \; 7, \; \dots). \end{align*}

Обратите внимание, что функция f(n) возвращает индекс i , который мы потом используем для выбора символьной функцииs_i(n), чтобы сгенерировать n-ный член последовательности. Поэтому можно назвать f(n) индексной функцией.

Индикаторные функции

Изменим порядок случаев и условий индексной функции f(n), чтобы легче находить интересные паттерны:

f(n) = \begin{cases} 0 & \text{if } 5 \nmid n \text{ and } 3 \nmid n, \\ 1 & \text{if } 5 \nmid n \text{ and } 3 \mid n, \\ 2 & \text{if } 5 \mid n \text{ and } 3 \nmid n, \\ 3 & \text{if } 5 \mid n \text{ and } 3 \mid n. \end{cases}

Эта функция помогает нам выбрать другую функцию s_{f(n)}(n), которая, в свою очередь, определяет n-ный член последовательности Fizz Buzz. Теперь наша цель заключается в том. чтобы заменить эту кусочную функцию единым выражением в аналитическом виде. Для этого мы сначала определим индикаторные функции I_m(n):

I_m(n) = \begin{cases} 1 & \text{if } m \mid n, \\ 0 & \text{if } m \nmid n. \end{cases}

Формулу f(n) теперь можно переписать так:

f(n) = \begin{cases} 0 & \text{if } I_5(n) = 0 \text{ and } I_3(n) = 0, \\ 1 & \text{if } I_5(n) = 0 \text{ and } I_3(n) = 1, \\ 2 & \text{if } I_5(n) = 1 \text{ and } I_3(n) = 0, \\ 3 & \text{if } I_5(n) = 1 \text{ and } I_3(n) = 1. \end{cases}

Замечаете паттерн? Вот та же функция, записанная в виде таблицы:

I_5(n)

I_3(n)

f(n)

0

0

0

0

1

1

1

0

2

1

1

3

Видите? Если считать значения в первых двух столбцах двоичными, а значения в третьем — десятичными значениями, то в каждой строке первые два столбца дают нам двоичное представление числа в третьем столбце. Например, 3_{10} = 11_2, и в последней строке таблицы мы действительно видим в первых двух столбцах биты 1 и 1, а в последнем — число 3. Иными словами, запись рядом двоичных цифр I_5(n) и I_3(n) даёт нам двоичное представление f(n). Следовательно, f(n) = 2 \, I_5(n) + I_3(n). Теперь можно написать небольшую программу для демонстрации этой формулы:

for n in range(1, 101):
    s = [n, 'Fizz', 'Buzz', 'FizzBuzz']
    i = (n % 3 == 0) + 2 * (n % 5 == 0)
    print(s[i])

Можно ещё сильнее укоротить её, немного снизив её понятность:

for n in range(1, 101):
    print([n, 'Fizz', 'Buzz', 'FizzBuzz'][(n % 3 == 0) + 2 * (n % 5 == 0)])

Итак, мы добились неплохих результатов. Хоть у нас пока нет универсального определения выражения в аналитическом виде, думаю, многие согласятся, что определённые выше индикаторные функции достаточно просты, чтобы допустить их в выражении в аналитическом виде.

Сложные показательные функции

В предыдущем разделе мы получили формулу f(n) = I_3(n) + 2 \, I_5(n), которую затем использовали в качестве индекса для поиска выводимого текста. Также мы заявили, что это уже достаточно хорошее выражение в аналитическом виде.

Однако в интересах усложнения задачи мы должны задаться вопросом: что, если использование индикаторных функций запрещено? Что, если мы должны придерживаться общепринятого определения выражения в аналитическом виде, допускающего только конечные сочетания таких базовых операций, как сложение, вычитание, умножение, деление, целочисленные степени и корни с целочисленными индексами, а также функции наподобие показательных, логарифмов и тригонометрических? Оказывается, что приведённую выше формулу можно переписать с использованием исключительно сложения, умножения и косинуса. Давайте приступим к преобразованию. Рассмотрим сумму

S_m(n) = \sum_{k = 0}^{m - 1} e^{i 2 \pi k n / m}

где i — мнимая единица, а n и m — целые. Это геометрический ряд на комплексной плоскости с коэффициентом r = e^{i 2 \pi n / m}. Если n кратно m, то n = cm для некого целочисленного c, и мы получаем r = e^{i 2 \pi n / m} = e^{i 2 \pi c} = 1. Следовательно, когда n кратно m, мы получаем

S_m(n) = \sum_{k = 0}^{m - 1} e^{i 2 \pi k n / m} = \sum_{k = 0}^{m - 1} 1^k = m

Если n не кратно m, то r \ne 1 , и геометрический ряд принимает вид

S_m(n) = \frac{r^m - 1}{r - 1} = \frac{e^{i 2 \pi n} - 1}{e^{i 2 \pi n / m} - 1} = 0

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

S_m(n) = \begin{cases} m & \text{if } m \mid n, \\ 0 & \text{if } m \nmid n. \end{cases}

Разделив обе части на m, получим

\frac{S_m(n)}{m} = \begin{cases} 1 & \text{if } m \mid n, \\ 0 & \text{if } m \nmid n. \end{cases}

Но правая часть — это I_m(n). Следовательно

I_m(n) = \frac{S_m(n)}{m} = \frac{1}{m} \sum_{k = 0}^{m - 1} e^{i 2 \pi k n / m}

Косинусы

Начнём с формулы Эйлера e^{i x} = \cos x + i \sin x, где x — вещественное число. Из этой формулы мы получаем e^{i x} + e^{-i x} = 2 \cos x. Следовательно

\begin{align*} I_3(n) &= \frac{1}{3} \sum_{k = 0}^2 e^{i 2 \pi k n / 3} \\ &= \frac{1}{3} \left( 1 + e^{i 2 \pi n / 3} + e^{i 4 \pi n / 3} \right) \\ &= \frac{1}{3} \left( 1 + e^{i 2 \pi n / 3} + e^{-i 2 \pi n / 3} \right) \\ &= \frac{1}{3} + \frac{2}{3} \cos \left( \frac{2 \pi n}{3} \right). \end{align*}

Третье равенство следует из того, что

e^{i 4 \pi n / 3} = e^{i 6 \pi n / 3} e^{-i 2 \pi n / 3} = e^{i 2 \pi n} e^{-i 2 \pi n/3} = e^{-i 2 \pi n / 3}

когда n целое.

Представленная выше функция определена для целочисленных значений n, но можно разложить эту формулу на вещественные x и составить график, чтобы наблюдать его форму в целых числах. Как и ожидается, функция имеет значение 1, когда x — кратное трём целое, и значение 0, когда x — целое, не кратное трём.

Graph
График \frac{1}{3} + \frac{2}{3} \cos \left( \frac{2 \pi x}{3} \right)

Аналогично

\begin{align*} I_5(n) &= \frac{1}{5} \sum_{k = 0}^4 e^{i 2 \pi k n / 5} \\ &= \frac{1}{5} \left( 1 + e^{i 2 \pi n / 5} + e^{i 4 \pi n / 5} + e^{i 6 \pi n / 5} + e^{i 8 \pi n / 5} \right) \\ &= \frac{1}{5} \left( 1 + e^{i 2 \pi n / 5} + e^{i 4 \pi n / 5} + e^{-i 4 \pi n / 5} + e^{-i 2 \pi n / 5} \right) \\ &= \frac{1}{5} + \frac{2}{5} \cos \left( \frac{2 \pi n}{5} \right) + \frac{2}{5} \cos \left( \frac{4 \pi n}{5} \right). \end{align*}

Разложив это выражение на вещественные значения x, мы тоже можем создать её график. В этом случае функция тоже получает значение 1 при целых x, кратных 5 , и 0, если x не кратно 5.

Graph
График \frac{1}{5} + \frac{2}{5} \cos \left( \frac{2 \pi x}{5} \right) + \frac{2}{5} \cos \left( \frac{4 \pi x}{5} \right)

Вспомним, что мы выразили f(n), как f(n) = I_3(n) + 2 \, I_5(n). Подставив эти тригонометрические выражения, получим

f(n) = \frac{1}{3} + \frac{2}{3} \cos \left( \frac{2 \pi n}{3} \right) + 2 \cdot \left( \frac{1}{5} + \frac{2}{5} \cos \left( \frac{2 \pi n}{5} \right) + \frac{2}{5} \cos \left( \frac{4 \pi n}{5} \right) \right)

Это можно упростить до

f(n) = \frac{11}{15} + \frac{2}{3} \cos \left( \frac{2 \pi n}{3} \right) + \frac{4}{5} \cos \left( \frac{2 \pi n}{5} \right) + \frac{4}{5} \cos \left( \frac{4 \pi n}{5} \right)

Это выражение тоже можно разложить на вещественные x и составить её график. Получившаяся кривая принимает в целочисленных точках значения 0, 1, 2 и 3, как нам и было нужно.

Graph
График \frac{11}{15} + \frac{2}{3} \cos \left( \frac{2 \pi x}{3} \right) + \frac{4}{5} \cos \left( \frac{2 \pi x}{5} \right) + \frac{4}{5} \cos \left( \frac{4 \pi x}{5} \right)

Теперь мы можем написать следующую программу на Python:

from math import cos, pi
for n in range(1, 101):
    s = [n, 'Fizz', 'Buzz', 'FizzBuzz']
    i = round(11 / 15 + (2 / 3) * cos(2 * pi * n / 3)
                      + (4 / 5) * cos(2 * pi * n / 5)
                      + (4 / 5) * cos(4 * pi * n / 5))
    print(s[i])

Дискретное преобразование Фурье

Внимательный читатель может заметить, что выражение, полученное нами для f(n) — это дискретный ряд Фурье. Это неудивительно, ведь вывод программы Fizz Buzz зависит только от n \bmod 15. Любую функцию для конечной циклической группы можно точным образом записать, как конечное разложение в ряд Фурье. В этом разделе мы получим f(n) при помощи дискретного преобразования Фурье. Стоит отметить, что представленные здесь вычисления вручную выполнять довольно скучно. Тем не менее, мы вкратце разберём, как они выполняются. В конце мы придём к точно такой же f(n), что и выше. Мы просто получим тот же результат более прямым, но и более трудоёмким образом. Если вам это не кажется интересным, можете пропустить этот раздел.

Мы знаем, что f(n) — периодическая функция с периодом 15. Чтобы применить дискретное преобразование Фурье, мы рассмотрим один полный период функции со значениями n = 0, 1, \dots, 14. В рамках этого периода мы получаем:

\begin{array}{c|ccccccccccccccc} n & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 & 10 & 11 & 12 & 13 & 14 \\ \hline f(n) & 3 & 0 & 0 & 1 & 0 & 2 & 1 & 0 & 0 & 1 & 2 & 0 & 1 & 0 & 0 \end{array}

Дискретный ряд Фурье f(n) — это

f(n) = \sum_{k = 0}^{14} c_k \, e^{i 2 \pi k n / 15}

где коэффициенты Фурье c_k задаются дискретным преобразованием Фурье

c_k = \frac{1}{15} \sum_{n = 0}^{14} f(n) e^{-i 2 \pi k n / 15}

для k = 0, 1, \dots, 14. Формула для c_k называется дискретным преобразованием Фурье (discrete Fourier transform, DFT). Формула для f(n) называется обратным дискретным преобразованием Фурье (inverse discrete Fourier transform, IDFT).

Пусть \omega = e^{-i 2 \pi / 15}. Тогда при использовании значений f(n) из представленной выше таблицы DFT принимает следующий вид:

c_k = \frac{3 + \omega^{3k} + 2 \omega^{5k} + \omega^{6k} + \omega^{9k} + 2 \omega^{10k} + \omega^{12k}}{15}

Подставив k = 0, 1, 2, \dots, 14 в это выражение, получим следующие коэффициенты Фурье:

\begin{align*} c_{0} &= \frac{11}{15}, \\ c_{3} &= c_{6} = c_{9} = c_{12} = \frac{2}{5}, \\ c_{5} &= c_{10} = \frac{1}{3}, \\ c_{1} &= c_{2} = c_{4} = c_{7} = c_{8} = c_{11} = c_{13} = c_{14} = 0. \end{align*}

Вычисление этих коэффициентов Фурье вручную может быть довольно монотонным процессом. На практике они почти всегда вычисляются математическим ПО, системами компьютерной алгебры или даже простым кодом наподобие такого: fizz-buzz-fourier.py. После вычисления коэффициентов мы можем подставить их в обратное преобразование и получить

\begin{align*} f(n) &= \sum_{k = 0}^{14} c_k \, e^{i 2 \pi k n / 15} \\[1.5em] &= \frac{11}{15} + \frac{2}{5} \left( e^{i 2 \pi \cdot 3n / 15} + e^{i 2 \pi \cdot 6n / 15} + e^{i 2 \pi \cdot 9n / 15} + e^{i 2 \pi \cdot 12n / 15} \right) \\ & \phantom{=\frac{11}{15}} + \frac{1}{3} \left( e^{i 2 \pi \cdot 5n / 15} + e^{i 2 \pi \cdot 10n / 15} \right) \\[1em] &= \frac{11}{15} + \frac{2}{5} \left( e^{i 2 \pi \cdot 3n / 15} + e^{i 2 \pi \cdot 6n / 15} + e^{-i 2 \pi \cdot 6n / 15} + e^{-i 2 \pi \cdot 3n / 15} \right) \\ & \phantom{=\frac{11}{15}} + \frac{1}{3} \left( e^{i 2 \pi \cdot 5n / 15} + e^{-i 2 \pi \cdot 5n / 15} \right) \\[1em] &= \frac{11}{15} + \frac{2}{5} \left( 2 \cos \left( \frac{2 \pi n}{5} \right) + 2 \cos \left( \frac{4 \pi n}{5} \right) \right) \\ & \phantom{=\frac{11}{15}} + \frac{1}{3} \left( 2 \cos \left( \frac{2 \pi n}{3} \right) \right) \\[1em] &= \frac{11}{15} + \frac{4}{5} \cos \left( \frac{2 \pi n}{5} \right) + \frac{4}{5} \cos \left( \frac{4 \pi n}{5} \right) + \frac{2}{3} \cos \left( \frac{2 \pi n}{3} \right). \end{align*}

Это то же самое выражение f(n), которое мы получили в предыдущем разделе. Мы видим, что индексную функцию Fizz Buzz f(n) можно точным образом выразить через механизмы анализа Фурье.

Заключение

Подведём итог: мы определили последовательность Fizz Buzz как

s_{f(n)}(n))_{n = 1}^{\infty}

где

f(n) = \frac{11}{15} + \frac{2}{3} \cos \left( \frac{2 \pi n}{3} \right) + \frac{4}{5} \cos \left( \frac{2 \pi n}{5} \right) + \frac{4}{5} \cos \left( \frac{4 \pi n}{5} \right)s_0(n) = ns_1(n) = \mathtt{Fizz}s_2(n) = \mathtt{Buzz}s_3(n) = \mathtt{FizzBuzz}

Программа на Python для вывода последовательности Fizz Buzz, основанная на этом определении, представлена выше. В более сжатом виде программу можно записать так:

from math import cos, pi
for n in range(1, 101):
    print([n, 'Fizz', 'Buzz', 'FizzBuzz'][round(11 / 15 + (2 / 3) * cos(2 * pi * n / 3) + (4 / 5) * (cos(2 * pi * n / 5) + cos(4 * pi * n / 5)))])

Также можно изящно обернуть это в однострочник оболочки на случай, если вы захотите поделиться этой программой с друзьями, чтобы удивить их:

python3 -c 'from math import cos, pi; [print([n, "Fizz", "Buzz", "FizzBuzz"][round(11/15 + (2/3) * cos(2*pi*n/3) + (4/5) * (cos(2*pi*n/5) + cos(4*pi*n/5)))]) for n in range(1, 101)]'

В этой статье мы превратили простую числовую игру в тригонометрическую конструкцию, состоящую из дискретного ряда Фурье с тремя косинусами и четырьмя коэффициентами. Ничто из этого не упрощает Fizz Buzz, наоборот, делает его только сложнее. Но это решение показывает, что каждое \mathtt{Fizz} и \mathtt{Buzz} теперь обязано своим существованием определённому множеству коэффициентов Фурье. Мы начали статью со скромной цели: усложнения этой простой задачи, и мне кажется, нам это вполне удалось.

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


  1. bolk
    28.11.2025 07:53

    Вашу исходную программу, кстати, можно упростить так:

    for n in range(1,101): print('Fizz'*(n%3==0)+'Buzz'*(n%5==0) or n)


  1. badsynt
    28.11.2025 07:53

    Мы начали статью со скромной цели: усложнения этой простой задачи, и мне кажется, нам это вполне удалось.

    Неплохо. Фурье мы любим.

    PS. Жемчужине моей коллекции уже почти 10 лет.

    https://habr.com/ru/articles/301536/


  1. enemigo
    28.11.2025 07:53

    от нечего делать строил раньше такое для классификации 16M цветов в Rainbow.Net total classifier