Рекуррентное соотношение рекурсии
Рекурсивная функция решает задачу через вызов самой себя для меньшего аргумента. Чтобы оценить результат работы или количество вызовов, составляют рекуррентное соотношение: формулу, связывающую значение для текущего аргумента со значением для меньшего аргумента.
Что такое рекуррентное соотношение
Сначала нужно внимательно прочитать рекурсивный алгоритм и определить, как изменяется параметр при каждом вызове. Затем записывают, что происходит с результатом текущего вызова и сколько рекурсивных вызовов он порождает. Обязательно учитывают базовый случай рекурсии: без него формула не имеет начального значения.
Рекуррентное соотношение — это формула, которая выражает величину \(F(n)\) через значения этой же величины для меньших аргументов: например, \(F(n)=F(n-1)+n\). Вместе с начальными условиями оно полностью описывает последовательность.
В задачах встречаются две разные величины. Первая — значение, которое возвращает функция, например сумма, произведение или число способов. Вторая — число вызовов функции. Они могут подчиняться разным формулам, поэтому их нельзя автоматически считать одной и той же последовательностью.
Если функция при \(n>0\) вызывает себя один раз с аргументом \(n-1\), то глубина цепочки равна \(n\) с точностью до выбранного базового случая. Если вызовов несколько, возникает дерево рекурсии, и число вызовов нужно складывать по всем ветвям. Этому помогает размер дерева рекурсивных вызовов.
Как составить формулу для результата
Удобный порядок действий такой:
- Выписать базовый случай и его результат: это начальное условие, например \(F(0)=1\).
- Рассмотреть один обычный вызов функции с аргументом \(n\).
- Заменить рекурсивный вызов его обозначением \(F(n-1)\), \(F(n-2)\) или другим подходящим выражением.
- Добавить действия, которые выполняются до или после рекурсивного вызова.
- Проверить формулу на небольшом аргументе вручную.
Например, функция может возвращать сумму чисел от \(1\) до \(n\). Если сначала вызывается функция для \(n-1\), а затем к её результату прибавляется \(n\), получается \(F(n)=F(n-1)+n\). Если результат умножается на \(n\), получается \(F(n)=n\cdot F(n-1)\). Если функция возвращает константу, добавленную к результату, эта константа также входит в соотношение.
Действия текущего вызова записываются вокруг результата рекурсивного вызова. Вызов f(n-1) обозначается как \(F(n-1)\), а не как новый неизвестный объект. Начальное условие берётся непосредственно из базового случая.
Как считать число вызовов
При подсчёте вызовов уточните, считается ли самый первый вызов. В экзаменационных задачах обычно спрашивают общее число входов в функцию, включая первоначальный вызов и вызовы в базовом случае. Если в условии сказано «число рекурсивных вызовов», иногда нужно считать только вызовы, сделанные внутри функции, без первоначального запуска.
Здесь \(1\) — текущий вызов, а \(n_1,\ldots,n_k\) — аргументы всех рекурсивных вызовов, сделанных из него. Если рекурсивных вызовов нет, сумма отсутствует. Например, для одного вызова с аргументом \(n-1\) имеем \(C(n)=1+C(n-1)\).
Для двух одинаковых вызовов f(n-1) формула будет \(C(n)=1+2C(n-1)\). Для вызовов f(n-1) и `f(n-2)\(—\)C(n)=1+C(n-1)+C(n-2)$. Такая запись особенно важна для алгоритмов, где одна и та же подзадача вычисляется много раз.
Не путайте число строковых выполнений и число вызовов функции. Если один вызов делает два рекурсивных вызова, количество вызовов не увеличивается на 2 один раз: каждый из этих вызовов сам может породить собственное дерево.
Функция при \(n>0\) вызывает себя один раз с аргументом \(n-1\), а затем возвращает результат, увеличенный на 3. Какова формула для результата?
Разобранный пример: два рекурсивных вызова
Рассмотрим функцию, которая при \(n>0\) вызывает себя для \(n-1\) и ещё раз для \(n-2\), а затем складывает результаты. Базовый случай: при \(n\le 0\) функция сразу возвращает 1. Требуется составить формулу для числа вызовов при начальном аргументе \(n\) и найти это число для \(n=4\).
1function F(n) 2 if n <= 0 then 3 return 1 4 return F(n - 1) + F(n - 2)
При начальном аргументе \(4\) функция вызывается 15 раз. В это число входят первоначальный вызов, все внутренние вызовы и вызовы с аргументами \(0\) и меньше. Если бы требовалось считать только вызовы, сделанные внутри функции, без первого запуска, ответ был бы \(14\).
Формулу результата для этого же фрагмента можно записать отдельно. Пусть \(R(n)\) — возвращаемое значение. Тогда \(R(n)=1\) при \(n\le0\), а при \(n>0\) выполняется \(R(n)=R(n-1)+R(n-2)\). Это похоже на последовательность Фибоначчи, но начальные значения здесь зависят от базового случая.
Проверка и связь с другими задачами
В задачах ЕГЭ полезно построить небольшую таблицу значений. Столбцы могут содержать аргумент, значение функции и число вызовов. Для каждого нового аргумента используйте только уже посчитанные меньшие значения. Такой способ надёжнее, чем попытка сразу угадать закономерность.
| n | C(n) для примера | Почему |
|---|---|---|
| -1 | 1 | Базовый случай |
| 0 | 1 | Базовый случай |
| 1 | 3 | Текущий вызов и два базовых |
| 2 | 5 | 1+3+1 |
| 3 | 9 | 1+5+3 |
| 4 | 15 | 1+9+5 |
Если изменяется не аргумент функции, а положение объекта, например клетка Кузнечика, нужно сначала точно описать состояние. Для таких задач полезны страницы шаг Кузнечика, конечная клетка Кузнечика и рекуррентная формула маршрутов Кузнечика. При наличии запрещённых клеток условие для них обычно равно нулю, что рассматривается на странице запрещённая клетка Кузнечика.
Сначала подпишите, что именно обозначает каждая буква: результат, число вызовов или число маршрутов. Затем отдельно выпишите базовый случай. Большинство ошибок появляется из-за смешения этих величин или пропуска вызова с базовым аргументом.
Quick-test
Проверь себя
Главное
- Рекуррентное соотношение связывает величину для n с величинами для меньших аргументов.
- Для результата учитывают действие текущего вызова над результатом рекурсивного вызова.
- Для числа вызовов добавляют 1 за текущий вызов и складывают размеры всех рекурсивных поддеревьев.
- Базовый случай задаёт начальные значения и прекращает рекурсию.
- Перед решением уточняйте, входит ли первоначальный вызов в подсчёт, и проверяйте формулу на малых аргументах.