Перебор, в котором каждое подсостояние считается ровно один раз. Превращает экспоненциальный перебор в полиномиальный, если подзадачи перекрываются.
Ключевые тезисы
- Нужны два свойства: оптимальная подструктура и перекрывающиеся подзадачи.
- Записать можно сверху вниз с мемоизацией или снизу вверх таблицей — сложность одна, удобство разное.
- Память часто сокращается до одной-двух строк таблицы, если переход смотрит только на соседние.
Какую задачу решает
Есть задачи, где перебор растёт экспоненциально, но сами подзадачи повторяются. Наивная считает одно и то же подсостояние тысячи раз; динамика замечает это и запоминает ответ — экспонента превращается в произведение «число состояний на стоимость перехода».
Главная трудность не в коде, а в постановке: придумать, что считать состоянием и как выразить его через меньшие. Как только это сформулировано, реализация занимает десять строк — поэтому разбор построен вокруг состояния, перехода и порядка обхода.
Подробный разбор
Наивная для числа способов подняться по лестнице вызывает себя дважды на каждом шаге: получается дерево из вызовов. При этом различных вопросов в нём всего — «сколькими способами дойти до ступеньки k». Каждый из них решается заново тысячи раз.
То же самое в задачах о расстоянии между строками, о рюкзаке, о разбиениях: экспоненциальный перебор при полиномиальном числе действительно разных подзадач.
Каждое подсостояние считать ровно один раз и запоминать ответ.
Экспонента превращается в произведение: число состояний, умноженное на стоимость одного перехода. Вся сложность задачи переезжает из кода в выбор состояния — и именно этот выбор и есть настоящая работа.
ступеньки: 1 2 3 4
dp[1] = 1
dp[2] = 2
dp[3] = dp[2] + dp[1] = 3
dp[4] = dp[3] + dp[2] = 5
большая задача собрана из двух уже решённых меньших "" A C
"" 0 0 0
A 0 1 1
B 0 1 1
C 0 1 2
dp[i][j] -- ответ для первых i символов первой строки и первых j второй
символы совпали -> dp[i-1][j-1] + 1
не совпали -> max(dp[i-1][j], dp[i][j-1])Таблица заполняется сверху вниз и слева направо, потому что каждая клетка смотрит только вверх, влево и по диагонали — то есть на уже посчитанное. Ответ — в правом нижнем углу, а сам путь восстанавливается обратным ходом по таблице.
from functools import lru_cache
def distance(a, b):
@lru_cache(maxsize=None)
def go(i, j):
if i == 0: return j # осталось вставить j символов
if j == 0: return i # осталось удалить i символов
if a[i - 1] == b[j - 1]:
return go(i - 1, j - 1)
return 1 + min(
go(i - 1, j), # удаление
go(i, j - 1), # вставка
go(i - 1, j - 1), # замена
)
return go(len(a), len(b))- Состояние — набор параметров, однозначно описывающий подзадачу.
- Переход — формула, выражающая ответ состояния через меньшие состояния.
- База — состояния, ответ для которых известен без вычислений.
- Порядок обхода — любой, при котором каждое состояние считается после тех, на кого ссылается.
- Память часто сокращается до одной-двух строк таблицы, если переход смотрит только на соседние.
Нужны два свойства, и оба существенны. Оптимальная подструктура: оптимальное решение задачи собирается из оптимальных решений подзадач — иначе запоминать ответ подзадачи бессмысленно, он может не подойти. Перекрывающиеся подзадачи: одни и те же подзадачи встречаются многократно — иначе кеш не даёт ничего, и это обычное разделяй-и-властвуй.
Отсюда же оценка сложности. Каждое состояние вычисляется один раз, значит суммарная работа — число состояний, умноженное на стоимость перехода. Именно поэтому «плохое» состояние (слишком много параметров) убивает решение: дело не в коде, а в размере пространства.
Порядок обхода — не стилистика, а часть корректности: клетка, читающая ещё не заполненную соседку, вернёт значение по умолчанию и испортит всю таблицу молча.
| Сигнал в условии | Вид динамики |
|---|---|
| «Сколькими способами можно…» | счётная: складываем варианты |
| «Максимальная выгода при последовательности выборов» | оптимизационная: максимум по переходам |
| «Можно ли набрать ровно X» | булева: рюкзак или подмножества |
| «Минимум операций, чтобы превратить A в B» | таблица по двум последовательностям |
| «Подпоследовательность» — элементы не подряд | динамика, а не |
| и перебор подмножеств | динамика по маскам |
| Жадный критерий ломается контрпримером | динамика вместо жадности |
Практический : наивная уже написана, и в ней видно, что одни и те же аргументы приходят снова и снова. Дальше достаточно добавить кеш — к таблице переходят потом, если понадобится.
| Тип | Состояние | Классические задачи |
|---|---|---|
| Линейная (1D) | dp[i] — ответ для префикса длины i | лестница, максимальная сумма подотрезка |
| По двум последовательностям | dp[i][j] | общая подпоследовательность, |
| Рюкзак | dp[i][w] | рюкзак, размен монет, разбиение на равные части |
| Интервальная | dp[l][r] | перемножение , палиндромные разрезания |
| По подмножествам | dp[mask] | коммивояжёр при , назначения |
| На дереве | dp[v] — ответ в поддереве | независимое множество, диаметр |
| По цифрам | dp[позиция][прижат ли к границе] | сколько чисел до N удовлетворяют условию |
Два параметра реализации, не меняющие сути: сверху вниз с мемоизацией (пишется быстрее, считает только нужные состояния, упирается в глубину ) или снизу вверх таблицей (предсказуемая память, легко сокращается до пары строк).
- Неудачное состояние. Если в нём оказывается три и больше измерений, обычно часть информации выводится из остального — и её не нужно хранить.
- Забытая база. без базы падает по глубине, таблица без базы заполняется нулями и молча врёт.
- Порядок заполнения. Клетка, читающая ещё не посчитанную, — самая незаметная ошибка в табличном варианте.
- Восстановление ответа. Число найдено, а сам набор предметов или сама строка — нет: для этого нужны либо указатели переходов, либо повторный проход по таблице.
- Память. Таблица на больших входах не помещается; если переход смотрит на предыдущую строку, храните две строки.
- Глубина рекурсии. на цепочке в миллион состояний упирается в лимит стека — переходите к таблице.
| Приём | Когда он лучше | Разница |
|---|---|---|
| Жадность | локальный выбор доказуемо безопасен | не перебирает варианты — проще и быстрее |
| Разделяй и властвуй | подзадачи не пересекаются | запоминать нечего |
| Перебор с возвратом | состояния уникальны, нужен сам набор решений | кеш не помогает |
| Обход в ширину по состояниям | нужен минимум шагов, переходы равноценны | вместо таблицы |
| Префиксные суммы | повторно считается сумма, а не подзадача | дешевле и проще |
- Назовите два свойства задачи, без которых динамика бессмысленна.
- Лестница: во что превращается решение, если убрать кеш, и почему?
- : что означает клетка
dp[i][j]и откуда в неё приходят значения? - Рюкзак: как восстановить не только максимальную стоимость, но и сам набор предметов?
- Когда хуже таблицы, а когда лучше?
Чем заменяют
Видеолекции
Записи университетских курсов, где эта тема звучит. Где у записи есть тайм-коды, ссылка открывает её с нужной секунды; где их не проставили — с начала.
Связанные темы
Алгоритмическое мышление
Problem Pattern Recognition85%
Как распознать паттерн задачи · Алгоритмы и структуры данныхУровень над алгоритмами: по формулировке условия понять, какой приём здесь работает. «Отсортированный массив», «подряд идущие элементы», «ближайший больший справа» — каждая такая фраза указывает на конкретное семейство решений.
Time and Space Complexity85%
Асимптотическая сложность · Алгоритмы и структуры данныхСпособ сравнить алгоритмы, не запуская их: как растёт время работы с ростом входа. Отбрасывает константы и оставляет то, что решает на больших данных.
Recursion and Divide and Conquer85%
Рекурсия и разделяй-и-властвуй · Алгоритмы и структуры данныхЗадача сводится к таким же задачам меньшего размера. Основа сортировки слиянием, быстрой сортировки, обхода деревьев и почти всей динамики.
Greedy Algorithms85%
Жадные алгоритмы · Алгоритмы и структуры данныхНа каждом шаге берём локально лучший вариант и не пересматриваем решение. Работает не всегда — но когда работает, проще и быстрее динамики.
Binary Search85%
Бинарный поиск · Алгоритмы и структуры данныхПоловинное деление на отсортированных данных: за логарифм шагов вместо линейного перебора. Приём шире поиска — им ищут ответ в любой монотонной задаче.
Всё, что названо в описании, разобрано здесь же или в соседней теме — переходы под каждым определением.
Состояние
Набор параметров, однозначно описывающий подзадачу. От него зависит всё.
Разбор ниже: ФормализацияОптимальная подструктура
Оптимум задачи собирается из оптимумов подзадач — иначе запоминать нечего.
Разбор ниже: Почему это работаетПерекрывающиеся подзадачи
Одна и та же подзадача встречается многократно: именно это и кешируется.
Разбор ниже: Почему это работаетСемь типов динамики
Линейная, по двум строкам, рюкзак, интервальная, по маскам, на дереве, по цифрам.
Разбор ниже: Вариации и параметрыТема встречается в тесте по направлениям — ошибки приведут обратно на эту страницу.
Глава «Алгоритмы и структуры данных» последний раз правилась . Нашли ошибку — напишите.