Признак в условии
- «сколькими способами», «максимум / минимум при выборе»
- решение для n выражается через решения для меньших входов
- наивная рекурсия пересчитывает одно и то же (экспонента)
Порядок действий
- Что такое состояние? dp[i] - ответ для префикса длины i / для суммы i.
- Рекуррента: как dp[i] выражается через предыдущие.
- База: dp[0] и краевые значения.
- Порядок обхода: снизу вверх или мемоизация сверху вниз.
- Свернуть память: если нужны только dp[i−1] и dp[i−2] - хватит двух переменных.
Шаблон
const dp = new Array(n + 1).fill(Infinity); // недостижимое, а не 0
dp[0] = 0;
for (let i = 1; i <= n; i++) {
for (const step of варианты) {
if (step <= i) dp[i] = Math.min(dp[i], dp[i - step] + 1);
}
}
let prev = 1, cur = 1; // свёрнутая память
for (let i = 2; i <= n; i++) [prev, cur] = [cur, prev + cur];
Сложность
Обычно O(состояний × переходов), память O(состояний) или O(1) после свёртки.
Частые ошибки
- инициализация нулём вместо бесконечности: недостижимые состояния выглядят достижимыми
- Кадане инициализируют нулём и ломаются на массиве из одних отрицательных
- кэш мемоизации создаётся заново на каждом вызове и не работает
- не замечают, что массив сворачивается до двух переменных
Мнемоника
«Состояние, переход, база, свёртка»
Не путать с
- Жадность - жадность работает, когда локально лучший выбор всегда часть глобального ответа. Есть контрпример - только динамика
- Перебор с возвратом - динамика - это перебор, у которого подзадачи повторяются; признак - «сколькими способами» и «максимум/минимум при ограничении»
Задачи на этот паттерн
- числа Фибоначчи с мемоизацией
- варианты подъёма по лестнице
- максимальная сумма подмассива
- минимум монет
- нельзя брать соседние
- наибольшая общая подпоследовательность
- число путей в сетке
- разбиение на слова
- число расшифровок
- минимальная сумма пути в сетке
- расстояние редактирования