Алгоритмы и структуры данных

Dynamic Programming

Динамическое программирование

актуальноТекущий рабочий стандарт

Направления: Основы · Алгоритмы и структуры данных

Перебор, в котором каждое подсостояние считается ровно один раз. Превращает экспоненциальный перебор в полиномиальный, если подзадачи перекрываются.

Ключевые тезисы

  • Нужны два свойства: оптимальная подструктура и перекрывающиеся подзадачи.
  • Записать можно сверху вниз с мемоизацией или снизу вверх таблицей — сложность одна, удобство разное.
  • Память часто сокращается до одной-двух строк таблицы, если переход смотрит только на соседние.

Какую задачу решает

Есть задачи, где перебор растёт экспоненциально, но сами подзадачи повторяются. Наивная считает одно и то же подсостояние тысячи раз; динамика замечает это и запоминает ответ — экспонента превращается в произведение «число состояний на стоимость перехода».

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

Подробный разбор

10 подтем — объяснения, формулы, примеры и интерактивные графики.

Наивная для числа способов подняться по лестнице вызывает себя дважды на каждом шаге: получается дерево из вызовов. При этом различных вопросов в нём всего — «сколькими способами дойти до ступеньки 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

большая задача собрана из двух уже решённых меньших
Сколькими способами подняться по лестнице шагами на 1 и 2
          ""   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])
Наибольшая общая подпоследовательность строк ABC и AC

Таблица заполняется сверху вниз и слева направо, потому что каждая клетка смотрит только вверх, влево и по диагонали — то есть на уже посчитанное. Ответ — в правом нижнем углу, а сам путь восстанавливается обратным ходом по таблице.

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 удовлетворяют условию

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

  • Неудачное состояние. Если в нём оказывается три и больше измерений, обычно часть информации выводится из остального — и её не нужно хранить.
  • Забытая база. без базы падает по глубине, таблица без базы заполняется нулями и молча врёт.
  • Порядок заполнения. Клетка, читающая ещё не посчитанную, — самая незаметная ошибка в табличном варианте.
  • Восстановление ответа. Число найдено, а сам набор предметов или сама строка — нет: для этого нужны либо указатели переходов, либо повторный проход по таблице.
  • Память. Таблица на больших входах не помещается; если переход смотрит на предыдущую строку, храните две строки.
  • Глубина рекурсии. на цепочке в миллион состояний упирается в лимит стека — переходите к таблице.

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

  1. Назовите два свойства задачи, без которых динамика бессмысленна.
  2. Лестница: во что превращается решение, если убрать кеш, и почему?
  3. : что означает клетка dp[i][j] и откуда в неё приходят значения?
  4. Рюкзак: как восстановить не только максимальную стоимость, но и сам набор предметов?
  5. Когда хуже таблицы, а когда лучше?

Чем заменяют

Видеолекции

Записи университетских курсов, где эта тема звучит. Где у записи есть тайм-коды, ссылка открывает её с нужной секунды; где их не проставили — с начала.

Связанные темы

Алгоритмическое мышление

Problem Pattern Recognition85%

Как распознать паттерн задачи · Алгоритмы и структуры данных

Уровень над алгоритмами: по формулировке условия понять, какой приём здесь работает. «Отсортированный массив», «подряд идущие элементы», «ближайший больший справа» — каждая такая фраза указывает на конкретное семейство решений.

Time and Space Complexity85%

Асимптотическая сложность · Алгоритмы и структуры данных

Способ сравнить алгоритмы, не запуская их: как растёт время работы с ростом входа. Отбрасывает константы и оставляет то, что решает на больших данных.

Recursion and Divide and Conquer85%

Рекурсия и разделяй-и-властвуй · Алгоритмы и структуры данных

Задача сводится к таким же задачам меньшего размера. Основа сортировки слиянием, быстрой сортировки, обхода деревьев и почти всей динамики.

Greedy Algorithms85%

Жадные алгоритмы · Алгоритмы и структуры данных

На каждом шаге берём локально лучший вариант и не пересматриваем решение. Работает не всегда — но когда работает, проще и быстрее динамики.

Binary Search85%

Бинарный поиск · Алгоритмы и структуры данных

Половинное деление на отсортированных данных: за логарифм шагов вместо линейного перебора. Приём шире поиска — им ищут ответ в любой монотонной задаче.

Проверить себя

Тема встречается в тесте по направлениям — ошибки приведут обратно на эту страницу.

Следующий шагПроверить себя: Алгоритмы и структуры данныхТема встречается в этом тесте 3 раза. Ошибка приведёт обратно на эту страницу — с объяснением, что именно не сошлось.Перейти →

Глава «Алгоритмы и структуры данных» последний раз правилась . Нашли ошибку — напишите.