Динамическое программирование (dynamic programming, ДП) решает задачу через подзадачи меньшего размера и считает каждую подзадачу один раз, запоминая ответы в таблице.

С доски, со ссылкой на «неизвестного автора»: ДП — это когда есть задача, которую мы не умеем решать, и для решения мы её разбиваем на подзадачи, которые тоже не умеем решать.

Зачем нужно

Рекурсивная формула часто сама просится в код: . Но если подзадачи перекрываются, прямая Рекурсия пересчитывает одни и те же значения: fib(n − 2) вызывают и fib(n), и fib(n − 1). Число вызовов растёт экспоненциально. ДП заменяет повторный счёт чтением из таблицы — время становится полиномиальным.

Как работает

Не из лекции: схема

  1. Состояние — что хранится в F[N].
  2. Переход — формула через меньшие состояния (на доске — инвариант ДП).
  3. База — граничные условия.
  4. Порядок вычисления — так, чтобы нужное уже было посчитано.
  5. Ответ — какое состояние отвечает на вопрос.

Переход обычно доказывают разбором по последнему шагу: последний прыжок кузнечика, последняя ступень, последняя монета. Корректность всего решения — индукцией по состояниям.

Три способа на Фибоначчи

, .

fib(n):  если n ≤ 2: return 1
         return fib(n − 1) + fib(n − 2)          // рекурсивный
 
table[1] = table[2] = 1                          // табличный
for i = 3 ... N: table[i] = table[i − 1] + table[i − 2]
 
a = b = 1                                        // линейный
for i = 3 ... N: c = a + b; a = b; b = c
ПамятьВремяПростотаХранение промежуточных данных
Рекурсивный±−+−
Табличный−±±+
Линейный++−−

Не из лекции

Рекурсивный — времени (вызовов ровно ), табличный — времени и памяти, линейный — времени и памяти. Промежуточный вариант — мемоизация: та же рекурсия, но посчитанные значения запоминаются, и повторный вызов сразу возвращает ответ. Это ДП «сверху вниз», таблица — «снизу вверх».

Одномерная динамика

ЗадачаПереходБаза
Кузнечик : число способов
Кузнечик ,
Лесенка: минимум ударов,
Панда: максимум пельменей,
Монеты: минимум монет

Двумерная динамика

Состояние — пара чисел, таблица двумерная.

  • Сочетания: , .
  • Разбиения на слагаемые: F[i][k] — число разбиений с максимальным слагаемым ровно : ; при , при .

Одномерная попытка для разбиений учитывает порядок слагаемых и не даёт ответ. Второе измерение («максимальное слагаемое») фиксирует порядок — слагаемые по невозрастанию.

Тонкости

  • ДП — не любое «разбить на подзадачи». У сортировки слиянием и бинарного поиска подзадачи не повторяются: это «разделяй и властвуй». На лекции они названы примерами ДП, см. конспект.
  • Рекурсия по формуле ДП без таблицы — тоже экспонента: на доске записано именно так.
  • Граничных условий нужно столько, насколько далеко назад заглядывает формула.
  • В задачах на число способов : пустой путь — один способ.
  • ДП находит оптимум там, где Жадный алгоритм ошибается: он перебирает все варианты последнего шага, а не только лучший на вид.

Частые ошибки

  • Писать рекурсию по формуле и не запоминать ответы.
  • Забыть граничное условие или задать его неверно ( вместо 1 в подсчёте способов).
  • Считать в неправильном порядке — обращаться к ещё не посчитанной ячейке.
  • Выбрать состояние, которое не различает нужное: одномерные разбиения считают и дважды.