Курс№16. Рекурсивные функции0%
Теория · номер 16 из 27

Рекурсивные функции

Функция задана через саму себя, нужно найти её значение или посчитать подходящие n. Пишем определение на Python и знаем, что делать с большими числами.

10 разделов~40 минут1 первичный балл
  1. 1Что проверяет номер
  2. 2Шаблон из 5 шагов
  3. 3Тип 1. Сколько n, для которых F(n) = k
  4. 4Тип 2. Две функции
  5. 5Тип 3. Большие числа, одна функция
  6. 6Тип 4. Большие числа, две функции
  7. 7Тип 5. Три функции
  8. 8Как решать
  9. 9Ловушки
  10. 10Домашка
Раздел 1 из 10

Что проверяет номер

Такая запись пугает только с виду. Определение функции в условии почти дословно переводится на Python: каждая строка условия становится строкой if с return. Думать приходится, только когда числа огромные и обычный код не успевает.

Дана функция F(n) (иногда две-три), выраженная через саму себя: есть база и шаг. Спрашивают значение F(N), выражение из значений или «сколько n от 1 до K, для которых F(n) = k».

Простыми словами

Рекурсия похожа на очередь, где каждый спрашивает соседа впереди. Тебе надо узнать F(5), но по формуле для этого нужно знать F(4) и F(3). Те тоже спрашивают дальше, пока вопрос не дойдёт до базы: это случай, где ответ дан прямо в условии, например F(1) = 1. Потом ответы идут обратно по цепочке.

Без базы очередь была бы бесконечной, поэтому база в функции стоит первой.

Теперь про кэш, или lru_cache. Обычная рекурсия забывчивая: одно и то же F(3) она считает заново каждый раз, когда его спросят. При больших n таких повторов становятся миллионы, и код зависает. Кэш это блокнот: посчитал значение, записал, в следующий раз просто посмотрел.

Если n очень большое, очередь получается слишком длинной. Тогда идём наоборот: сами считаем значения по порядку в цикле, от маленьких к большим, и блокнот заполняется заранее. Это называется прогревом.

Раздел 2 из 10

Шаблон из 5 шагов

Эти шаги одинаковы для всех задач номера. Сначала перепиши определение, потом посмотри, насколько большие числа просят. От этого зависит, нужен ли кэш.

① Переписываем функцию как в условии, условия в том же порядке. ② print(F(26)). ③ Если медленно или слишком глубоко: from functools import lru_cache, перед функцией @lru_cache(None) (пишем перед рекурсией). ④ Прогрев: for n in range(4, 4024): F(n). Левая граница — ограничение (где кончается база), правая — большее число (самое большое нужное n). ⑤ Запасной вариант: import sys, sys.setrecursionlimit(3000).

python
from functools import lru_cache

@lru_cache(None)
def F(n):
    if n == 1:
        return 1
    if n % 2 == 0:
        return n + F(n - 1)
    if n > 1 and n % 2 != 0:
        return 2 * F(n - 2)

for n in range(4, 4024):       # прогрев от малого к большому
    F(n)
print(F(26))                   # 4122

Зачем нужен кэш: посчитаем вызовы

Рекурсия без кэша считает одно и то же много раз. Код ниже считает, сколько раз вызвалась функция с кэшем и без него:

python
from functools import lru_cache

calls = 0
def F(n):
    global calls
    calls += 1
    if n < 3:
        return 1
    return F(n - 1) + 2 * F(n - 2)

print(F(25), calls)

calls = 0
@lru_cache(None)
def G(n):
    global calls
    calls += 1
    if n < 3:
        return 1
    return G(n - 1) + 2 * G(n - 2)

print(G(25), calls)

Ответ один и тот же, а вызовов в тысячи раз меньше. Запусти и сравни сам. Если поднять 25 до 50, первая функция уже не дождётся ответа, а вторая посчитает мгновенно.

Попробуй самучебная задача

F(n) = 1 при n < 3, F(n) = F(n − 1) + 2·F(n − 2) при n ≥ 3. Чему равно F(20)?

Подсказка

Переведи определение на Python и добавь lru_cache, чтобы считалось быстро.

Разбор руками

Возьмём F(n) = 1 при n < 3 и F(n) = F(n − 1) + 2·F(n − 2) при n ≥ 3. Идём снизу вверх:

nF(n)
11
21
33
45
511
621

F(3) = 1 + 2·1 = 3, F(4) = 3 + 2·1 = 5, F(5) = 5 + 2·3 = 11. Рекурсия без кэша считает одни и те же значения снова и снова, поэтому при больших n она зависает. lru_cache запоминает готовые значения.

в условиив коде
F(n) = 1 при n < 3if n < 3: return 1
n / 2 при чётном nn // 2
при чётном nif n % 2 == 0:
значение выражения F(a) / F(b)F(a) // F(b)
Попробуй самучебная задача

F(n) = 2 при n < 2. При n ≥ 2: если n кратно 3, то F(n) = F(n − 1) + n, иначе F(n) = 2·F(n − 2) − 1. Чему равно F(30)?

Раздел 3 из 10

Тип 1. Сколько n, для которых F(n) = k

Здесь функцию вызывают не один раз, а для каждого n из диапазона. Цикл и счётчик уже знакомы: проверяем значение и прибавляем единицу.

python
from functools import lru_cache

@lru_cache(None)
def F(n):
    if n == 0:
        return 0
    if n > 0 and n % 2 == 0:
        return F(n // 2)            # для целого результата пишем //
    if n % 2 != 0:
        return 1 + F(n - 1)

for n in range(10, 6300):           # прогрев
    F(n)
a = 0
for n in range(1, 501):
    if F(n) == 8:
        a += 1
print(a)                            # 5
Попробуй самучебная задача

F(0) = 0; F(n) = F(n // 2) при чётном n > 0; F(n) = 1 + F(n − 1) при нечётном n. Для скольких n от 1 до 1000 выполняется F(n) = 3?

Подсказка

Цикл по range(1, 1001) и счётчик.

Попробуй самучебная задача

Для той же функции из задания выше про чётные и нечётные n: при скольких n от 1 до 500 выполняется F(n) = 4?

Раздел 4 из 10

Тип 2. Две функции

Если функций две, просто напиши обе. Python разрешает им вызывать друг друга, порядок объявления не важен.

python
from functools import lru_cache

@lru_cache(None)
def F(n):
    return 2 * (g(n - 3) + 8)

@lru_cache(None)
def g(n):
    if n < 10:
        return 2 * n
    if n >= 10:
        return g(n - 8) + 1

for n in range(10, 16000):          # прогрев
    F(n)
print(F(15548))                     # 3936

Второй вариант прогрева: сначала for n in range(40000): g(n), потом for n in reversed(range(40000)): F(n). Иногда нужно ставить else (см. тип 3).

Попробуй самучебная задача

F(n) = n при n < 4, иначе F(n − 1) + G(n − 2). G(n) = 1 при n < 3, иначе 2·G(n − 1) − F(n − 3). Чему равно F(15)?

Раздел 5 из 10

Тип 3. Большие числа, одна функция

Если n измеряется десятками тысяч, рекурсия упрётся в ограничение глубины. Выход в том, чтобы считать значения снизу вверх в цикле: тогда каждое следующее значение уже опирается на готовое из кэша.

Значения огромные, поэтому кэш маленький: lru_cache(100). Работает, если расстояние между нужными числами не больше 100. Нужные значения запоминаем в переменные прямо в цикле, остальные прогреваем в else:

python
from functools import lru_cache

@lru_cache(100)
def F(n):
    if n < 10:
        return 1
    return (n + 2) * F(n - 2)

for n in range(10, 50004):
    if n == 50003:
        x1 = F(n)
    elif n == 50001:
        x2 = F(n)
    elif n == 49999:
        x3 = F(n)
    else:
        F(n)                        # без else кэш не прогреется
print((x1 // 7 - 11 * x2) // x3)

Деление //, не /: если при обычном / не получается, пиши //.

Попробуй самучебная задача

F(n) = 1 при n < 3, F(n) = n · F(n − 2) при n ≥ 3. Чему равно F(20000) // F(19998)?

Подсказка

Можно обойтись и без кода, если подставить определение.

Раздел 6 из 10

Тип 4. Большие числа, две функции

Главное здесь определить, куда смотрит каждая функция. От этого зависит направление прогрева: значение должно быть уже посчитано к моменту, когда оно понадобится.

Прогрев идёт по направлению зависимостей: функция, которая смотрит вверх (n + 11), греется по убыванию, которая смотрит вниз (n − 8) — по возрастанию.

python
from functools import lru_cache

@lru_cache(100)
def F(n):
    if n >= 21:
        return F(n - 8) + 900
    if n < 21:
        return 10 * (G(n - 7) - 20)

@lru_cache(100)
def G(n):
    if n >= 15000:
        return n // 17 + 25
    if n < 15000:
        return G(n + 11) - 4

for n in range(15000, 5, -1):       # G смотрит вверх: греем по убыванию
    G(n)
for n in range(21, 401):            # F смотрит вниз: греем по возрастанию
    F(n)
print(F(400))
Раздел 7 из 10

Тип 5. Три функции

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

Заготовка: три функции под @cache (from functools import *), потом три цикла подряд:

python
from functools import *

@cache
def F(n):
    ...

@cache
def g(n):
    ...

@cache
def Q(n):
    ...

for n in range(1, 1000000): Q(n)
for n in range(1000000, 1, -1): g(n)
for n in range(1, 1000000): F(n)
print(F(2026))

Порядок и направление циклов зависят от условия: сначала греем функцию, на которую опираются остальные, каждую в своём направлении. @cache и @lru_cache(None) — одно и то же.

Пример: Q(n) = 1 при n ≥ 2500, иначе Q(n + 1) + 3; G(n) = n при n ≤ 3, иначе G(n − 1) + Q(2n) % 7; H(n) = 1 при n ≤ 2, иначе H(n − 1) + G(n) − Q(n) % 4.

python
from functools import lru_cache

@lru_cache(None)
def Q(n):
    if n >= 2500: return 1
    return Q(n + 1) + 3

@lru_cache(None)
def G(n):
    if n <= 3: return n
    return G(n - 1) + Q(2 * n) % 7

@lru_cache(None)
def H(n):
    if n <= 2: return 1
    return H(n - 1) + G(n) - Q(n) % 4

for n in range(2500, 0, -1): Q(n)       # Q смотрит вверх — по убыванию
for n in range(1, 1501): G(n)
for n in range(1, 1501): H(n)
print(H(1500))
Раздел 8 из 10

Как решать

Короткий порядок действий на экзамене. Если сомневаешься, какой тип перед тобой, смотри на размер n.

  1. Переписываем функцию из условия.
  2. Зовёт себя два раза и больше или n большое — ставим @lru_cache(None) и прогреваем циклом.
  3. Две-три функции — прогрев по направлению зависимостей.
  4. Значения огромные — @lru_cache(100) с else: F(n) в цикле.
  5. Деление нацело //.
Раздел 9 из 10

Ловушки

Ловушка

Рекурсия без кэша на двух вызовах зависает.

Ловушка

Глубина больше 1000: нужен прогрев (или setrecursionlimit, шаг ⑤).

Ловушка

При прогреве с запоминанием значений ставь else: F(n), иначе кэш прогреется не на всех числах.

Ловушка

lru_cache(100) работает, если расстояние между нужными числами не больше 100.

Ловушка

/ вместо // даёт дробное число.

Раздел 10 из 10

Домашка

Задачи с настоящих экзаменов и из банка ФИПИ. Решай прямо здесь: кнопка «Решать здесь» открывает редактор, а код каждой задачи сохраняется.

Ещё задачи этого вида лежат в разделе «Задания», №16.

Задачи «Попробуй сам» учебные, в домашке задачи с настоящих экзаменов и из банка ФИПИ. Код в блоках можно менять и запускать, он выполняется прямо в браузере.