О чём лекция
Многие задачи сводятся к таким же задачам меньшего размера: число Фибоначчи — к двум предыдущим, путь кузнечика до клетки — к путям до и . Если решать такие подзадачи рекурсией «в лоб», одни и те же значения пересчитываются экспоненциально много раз. Динамическое программирование считает каждую подзадачу один раз и складывает ответы в таблицу. Лекция показывает три способа считать Фибоначчи, серию задач на одномерную динамику, двумерную — на сочетаниях и разбиениях числа — и пример, где жадный алгоритм ошибается, а динамика нет.
Как читать
Доказательства и асимптотики в блоках «Не из лекции» дописаны к конспекту: на лекции их не было. Всё остальное — с доски.
Маршрут
- Что такое ДП → Динамическое программирование
- Числа Фибоначчи тремя способами → Рекурсия
- Сравнение способов → Асимптотические оценки
- Одномерная динамика: кузнечик, лесенка, панда
- Двумерная динамика: число сочетаний
- Сложная динамика: разбиения на слагаемые
- Жадность против ДП → Жадный алгоритм
1. Определение
Д.П. — это когда есть задача, которую мы не умеем решать, для решения мы её разбиваем на подзадачи, которые тоже не умеем решать.
— @неизвестный автор
Примеры: MergeSort, Qsort, бинпоиск.
Это «разделяй и властвуй», а не ДП
Сортировка слиянием, Быстрая сортировка и Бинарный поиск действительно разбивают задачу на подзадачи, но подзадачи у них не повторяются: каждая половина массива сортируется один раз. Такой подход называют «разделяй и властвуй» (divide and conquer).
ДП нужно, когда подзадачи перекрываются — одна и та же встречается много раз, как в вычислении и , и . Тогда ответ выгодно запомнить. У сортировки слиянием запоминать нечего.
Не из лекции: из чего состоит решение динамикой
Все задачи лекции решаются по одной схеме:
- Состояние — что лежит в
F[N]: «число способов добраться до клетки », «минимум ударов до ступени ».- Переход — формула через меньшие состояния. На доске она названа инвариантом ДП.
- База — граничные условия: значения, которые известны без формулы.
- Порядок — считать так, чтобы к моменту
F[N]всё нужное уже было посчитано.- Ответ — какое состояние (или их комбинация) отвечает на вопрос.
Корректность обычно доказывается индукцией по состоянию, как Рекурсия: база верна, а переход верен, если верны меньшие состояния.
2. Числа Фибоначчи
Каждое число — сумма двух предыдущих.
Рекуррентная формула (инвариант ДП):
Граничные условия: , .
1) Рекурсивный способ
функция fib(n)
если n ≤ 2
return 1
иначе
return fib(n − 1) + fib(n − 2)Ответ: fib(n).
2) Табличный способ
table = [ ... ]
table[1] = table[2] = 1
for i = 3 ... N
table[i] = table[i − 1] + table[i − 2]Ответ: table[N].
3) Линейный способ
a = b = 1
c = 0
for i = 3 ... N
c = a + b
print(c)
a = b
b = cc ← ответ.
Для следующего числа нужны только два предыдущих — их и хранят. На
рисунке: A и B стоят на , C = A + B даёт , затем
A = B, B = C — окно из двух чисел сдвигается вправо.
Не из лекции: граничный случай линейного способа
При и цикл не выполняется, и
cостаётся 0, хотя . Надёжнее отвечатьb: после цикла это всегда при (при — тоже 1).
Не из лекции: почему рекурсия здесь медленная
Вызов
fib(n)вызываетfib(n − 1)иfib(n − 2), аfib(n − 1)снова вызываетfib(n − 2). Одни и те же значения пересчитываются заново.Число вызовов. Пусть — сколько вызовов делает
fib(n). , . Индукцией проверяется : база , переход . Проверено запуском:fib(30)= 832 040 делает 1 664 079 вызовов.Сколько это. растёт как , где . Хватает и грубой оценки: , значит, — время рекурсии экспоненциальное.
Табличный и линейный способы считают каждое один раз: времени. Память у таблицы , у линейного . У рекурсии память — глубина стека, .
3. Сравнение методов
| Память | Время | Простота | Хранение промежуточных данных | |
|---|---|---|---|---|
| Рекурсивный | ± | − | + | − |
| Табличный | − | ± | ± | + |
| Линейный | + | + | − | − |
Не из лекции: оценки за плюсами и минусами
Время Память Рекурсивный — экспонента на стек Табличный на таблицу Линейный «Хранение промежуточных данных» — плюс таблицы: после одного прогона известны все , и на много запросов можно отвечать сразу. Линейный способ помнит только последние два.
Рекурсию можно ускорить, не меняя её вида: запоминать уже посчитанные значения в таблице и не вызывать функцию повторно (мемоизация). Это ДП «сверху вниз», табличный способ — «снизу вверх».
4. Примеры
а) Кузнечик
Задача. Кузнечик прыгает на или . Сколько способов добраться из 0 в ?
⌒⌒⌒⌒ ⌒ . . . ⌒⌒ ▶
●───────────────────●────▶
0 NВ клетку можно попасть только из (прыжок ) или из (прыжок ):
Не из лекции: почему формула верна
Разделим все способы попасть в по последнему прыжку. Если он , до него кузнечик был в и попал туда любым из способов. Если — из , способов. Группы не пересекаются (последние прыжки разные) и покрывают всё — отсюда сумма.
: в 0 кузнечик уже стоит — один «пустой» путь. Без этого вышло бы 1 вместо 2 (прыжки и ).
Это числа Фибоначчи со сдвигом: . Проверено: .
б) Кузнечик +1, +2, +3
Нужны три граничных значения: формула заглядывает на три назад.
в) Лесенка
Задача: минимизировать сумму ударов плетью. На ступени —
cost[N] ударов, шаг или .
▶
2 ▄▄┃ ← ступень N
7 ┃
1 ┃
3 ┃
2 ┃
━━━┛F[N] — накопительное значение: минимум ударов, чтобы встать на
ступень . На неё приходят с или :
cost[N] — сколько ударов на -ой ступени.
Не из лекции: почему можно брать минимум
Оптимальный путь до приходит с или , и его часть до предпоследней ступени — тоже оптимальный путь до неё: иначе её можно было бы заменить на более дешёвую и улучшить весь путь. Значит, .
Пример по рисунку (ступени снизу вверх, похоже, 2, 3, 1, 7, 2): . Ответ 5: путь (). Ступени с тремя и семью ударами перепрыгиваем.
Время , память , а если нужен только ответ — , как у линейного Фибоначчи.
г) Панда и пельмешки
Больше пельменей!!! Больших!
Та же лесенка, но теперь на ступенях пельмени и их нужно собрать как можно больше:
Не из лекции
Если все
costположительные, задача вырождается: выгодно наступить на каждую ступень, и ответ — просто сумма. По рисунку , и формула даёт то же. Интересной задача становится, когда бывают отрицательные значения — ступени, которые лучше перепрыгнуть.
5. Двумерная динамика
Число сочетаний:
А) По формуле
— цикл, .
Б) Рекуррентно
функ. c(n, k)
если k = 0 OR n = k
return 1
return c(n − 1, k) + c(n − 1, k − 1)Не из лекции: зачем второй способ
Переполнение. Формула с факториалами считает огромные промежуточные числа: , и
long longпереполняется, хотя сам помещается даже вint. Рекуррентный способ только складывает, и промежуточные значения не больше ответа.Почему формула верна. Выбираем предметов из . Последний предмет либо не взят — тогда все выбраны из остальных (), либо взят — тогда из остальных выбрано ().
Почему «двумерная». Состояние — пара , и таблица — двумерная: треугольник Паскаля. Но на доске записана рекурсия без таблицы, и она медленная по той же причине, что
fib: вызовов . Проверено: делает 310 235 039 вызовов. С таблицейtable[i][j] = table[i−1][j] + table[i−1][j−1]по строкам — времени.
6. Сложная динамика
Разбиение на натуральные слагаемые. Сколькими способами представить число суммой натуральных чисел, если порядок не важен?
4 = 1 + 1 + 1 + 1
4 = 2 + 1 + 1 = 1 + 2 + 1 = 1 + 1 + 2
4 = 2 + 2
4 = 1 + 3
4 = 4Строка 2 + 1 + 1 = 1 + 2 + 1 = 1 + 1 + 2 — это одно разбиение, записанное
в разном порядке. Всего у 4 пять разбиений.
а) Одномерная попытка
F[k] — количество способов разбить число :
Слагаемые подписаны как первое слагаемое разбиения: , , …, ; единица — само .
Учитываем порядок ⇒ не даёт ответ.
Не из лекции: что считает эта формула
Она считает упорядоченные суммы (композиции): и идут как разные. Из формулы , то есть . Для 4 выходит 8 вместо 5.
б) Двумерная
F[i][k] ← количество способов разбить на слагаемые, где максимальное
слагаемое .
Пример на доске: , , . Убираем максимальное слагаемое 3, остаётся 2 — его разбиения с максимумом не больше 3.
Не из лекции: доказательство и ответ
Порядок перестаёт мешать, если записывать слагаемые по невозрастанию: каждое разбиение тогда записывается единственным образом.
Переход. Разбиение с максимумом ровно при — это плюс разбиение остатка , у которого максимум . Наоборот, к любому такому разбиению остатка можно приписать — получится разбиение с максимумом . Это взаимно однозначно, поэтому .
База. : максимум больше суммы — 0 способов. : само число, один способ.
Ответ: разбиений числа всего . Проверено запуском для : 1, 2, 3, 5, 7, 11, 15, 22, 30, 42.
Порядок и время. Строки по возрастанию : использует только меньшие . Состояний , на каждое сумма из слагаемых: . Если хранить префиксные суммы , переход занимает , и всё — .
7. Жадное программирование vs динамическое
Пример: номиналы монет, выдать клиенту сумму — меньше монет. 6 рублей, номиналы .
Жадно:
- сортируем по убыванию;
- беру максимально, пока имеет смысл.
Динамика. Монеты . F[N] — количество монет:
Не из лекции: в чём пример
На доске выписан только жадный ответ — три монеты. Динамика находит две: . Проверено запуском: .
Жадный алгоритм берёт лучшее на текущем шаге (самую крупную монету) и никогда не пересматривает решение. Здесь первая же четвёрка портит ответ: остаток 2 набирается только единицами. Динамика перебирает все варианты последней монеты и берёт лучший — поэтому не ошибается.
Доказательство перехода. В оптимальном наборе для есть какая-то монета , остальные — набор для , и он тоже оптимален (иначе заменили бы его и улучшили весь). Перебрав все , найдём лучший.
Детали. Минимум берётся только по ; если сумму набрать нельзя, . Время .
Для обычных номиналов вроде жадность даёт оптимум, но это свойство конкретного набора, а не алгоритма. См. Жадный алгоритм.
Грабли
- Рекурсия без запоминания на перекрывающихся подзадачах —
экспоненциальное время:
fib(n), . - Граничных условий столько, на сколько назад смотрит формула: у кузнечика — три.
- в задачах на число способов — пустой путь тоже способ.
- Линейный Фибоначчи с ответом
cневерен при . - Одномерная динамика для разбиений считает упорядоченные суммы.
- Факториалы в формуле сочетаний переполняются задолго до ответа.
- Жадный выбор монет не всегда оптимален: , сумма 6.
- MergeSort и бинпоиск — не ДП, а «разделяй и властвуй»: подзадачи не повторяются.
После этой лекции должно получаться
- объяснить, чем ДП отличается от простой рекурсии;
- посчитать тремя способами и сравнить их по времени и памяти;
- доказать, что рекурсивный
fibэкспоненциален; - вывести формулу кузнечика через последний прыжок и объяснить ;
- решить лесенку на минимум и на максимум;
- посчитать без переполнения;
- объяснить, почему разбиения нужно считать двумерной динамикой, и вывести
формулу для
F[i][k]; - показать набор монет, на котором жадность ошибается, и решить задачу динамикой.