Бесплатная бета: 60 дней полного доступа, без карты.мест осталось: 173Зарегистрироваться бесплатно

Мы используем необходимые куки для работы сайта (вход и язык). Если ты согласишься, мы также загрузим Google Analytics, чтобы видеть, какие страницы читают, и Google reCAPTCHA для защиты форм обратной связи и сообщений об ошибке от спама. Политика конфиденциальности

← Blog
Как готовиться

Динамическое программирование: три шага, код на Python и разбор задач

· 2026-10-20 ·

Короткий ответ. Динамическое программирование это способ решать задачу через ответы на её меньшие версии. Ответ для n собирается из ответов для n - 1 и n - 2. Каждый маленький ответ считается один раз и запоминается. Так задачи «сколько способов», «минимальная стоимость», «максимальная выгода» решаются за O(n) или O(n²) вместо перебора. Ниже признаки, три шага, код на Python и разбор задач.

Что такое динамическое программирование?

Берём задачу, у которой большой ответ строится из маленьких. Определяем состояние: что именно значит «ответ для i». Пишем переход: как ответ для i получить из уже посчитанных.

Главное условие: маленькие задачи повторяются. Рекурсия без запоминания считала бы их снова и снова. Динамическое программирование считает каждую один раз.

Слово «динамическое» тут ничего не значит. Название историческое. Смысл в таблице ответов.

Наибольшая сумма сплошного отрезка
maxSubarraySum([-2, 1, -3, 4, -1, 2, 1, -5])
nums
-2
1
-3
4
-1
2
1
-5
curr на каждом i: лучшая сумма отрезка, который кончается на i
curr = -2best = -2

Ищем наибольшую сумму сплошного отрезка. Состояние на i: лучшая сумма отрезка, который кончается ровно на i.

Гонка скорости
Простая рекурсия и таблица снизу вверх на одной и той же лестнице из 18 ступенек

Столбики: сколько раз посчитан ответ для каждой ступеньки i, от 0 до 18.

Одна операция: один вызов или одна клетка таблицы. За каждый кадр все дорожки делают одинаковое число операций, поэтому медленная отстаёт.

Простая рекурсия O(2ⁿ)
операций: 0
Таблица снизу вверх O(n)
операций: 0

 

А если чисел больше

Операции для n чисел и примерное время при 100 миллионах простых операций в секунду. Это оценка по тому же счёту, а не замер.

n = 20
Простая рекурсия21,9 тыс. · меньше 1 мс
Таблица снизу вверх21 · меньше 1 мс
n = 40
Простая рекурсия331,2 млн · 3,3 с
Таблица снизу вверх41 · меньше 1 мс
n = 60
Простая рекурсия5 трлн · 14 ч
Таблица снизу вверх61 · меньше 1 мс

Страница паттерна: когда его применять и шаблон на шести языках

По каким словам в задаче узнать динамическое программирование?

  • «Сколькими способами можно», «число путей».
  • «Минимальная стоимость», «максимальная сумма без соседних».
  • «Можно ли набрать сумму из монет», «можно ли разбить на два равных».
  • «Самая длинная возрастающая подпоследовательность».

Общий признак: ответ для всего входа зависит от ответов для его частей. И частей мало. Если каждый выбор независим от прошлого, хватит жадного алгоритма. Если нужны все решения, а не лучшее, это бэктрекинг.

Из каких трёх шагов состоит решение?

  • Состояние. dp[i] это ответ для первых i элементов или для числа i.
  • Переход. Формула, которая выражает dp[i] через dp с меньшими индексами.
  • База. Значения dp для самых маленьких i, которые известны сразу.

Потом остаётся порядок вычисления. Считаем от базы к нужному ответу. Тогда каждый переход ссылается на готовые ячейки.

Как это выглядит на примере?

Задача Climbing Stairs: лестница из n ступенек. Шаг на одну или на две. Сколько способов подняться?

Состояние: dp[i] это число способов дойти до ступеньки i. Переход: dp[i] = dp[i - 1] + dp[i - 2]. На ступеньку i приходят либо с i - 1, либо с i - 2. База: dp[1] = 1, dp[2] = 2.

Для n = 5: dp = [_, 1, 2, 3, 5, 8]. Ответ 8. Это числа Фибоначчи, и это не совпадение.

Как написать динамическое программирование на Python?

def climb_stairs(n):
    if n <= 2:
        return n
    a, b = 1, 2                      # dp[i-2], dp[i-1]
    for _ in range(3, n + 1):
        a, b = b, a + b
    return b

def coin_change(coins, amount):
    INF = float("inf")
    dp = [0] + [INF] * amount        # dp[x] = минимум монет на сумму x
    for x in range(1, amount + 1):
        for c in coins:
            if c <= x and dp[x - c] + 1 < dp[x]:
                dp[x] = dp[x - c] + 1
    return -1 if dp[amount] == INF else dp[amount]

def rob(houses):
    take = skip = 0                  # лучший результат с последним взятым / пропущенным
    for h in houses:
        take, skip = skip + h, max(take, skip)
    return max(take, skip)

Первая функция хранит две переменные вместо массива. Если переход смотрит только на два шага назад, массив не нужен.

Чем восходящий способ отличается от нисходящего?

ПризнакСнизу вверх (таблица)Сверху вниз (мемоизация)
Форма кодаЦиклРекурсия с кешем
ПорядокНужно выбрать самомуРекурсия выберет сама
ПамятьЧасто можно ужать до O(1)Кеш плюс стек вызовов
Считает лишнееИногда все состоянияТолько нужные
РискОшибка в порядкеПереполнение стека

На собеседовании начинайте с рекурсии и кеша. Так проще найти переход. Потом переписывайте в цикл, если просят.

Как решить Coin Change?

Даны номиналы монет и сумма. Нужно минимальное число монет. Состояние: dp[x] это минимум монет на сумму x. Переход: для каждой монеты c пробуем dp[x - c] + 1. База: dp[0] = 0.

Жадный выбор самой большой монеты здесь ломается. Монеты [1, 3, 4], сумма 6. Жадность даёт 4 + 1 + 1, а ответ 3 + 3. Именно поэтому нужно динамическое программирование.

Как решить House Robber?

Ряд домов с деньгами. Нельзя брать два соседних. Нужна максимальная сумма.

Состояние: лучший результат для первых i домов. Переход: берём дом i и добавляем результат для i - 2. Или пропускаем и берём результат для i - 1. В коде выше это две переменные take и skip.

На каких ошибках ломается решение?

  • Состояние определено размыто, и переход не выражается через него.
  • База пропущена или неверна, ошибка расходится по всей таблице.
  • Порядок вычисления такой, что переход читает пустую ячейку.
  • Берут жадный алгоритм там, где нужен перебор состояний.

Как объяснить приём на собеседовании?

Назовите состояние вслух одной фразой. Потом переход. Потом базу. Только после этого пишите код. Собеседующий проверяет именно эти три фразы.

Сложность назовите как число состояний, умноженное на стоимость перехода. Для Coin Change это O(сумма · число монет).

Отработать узнавание приёма можно на странице динамическое программирование. Нисходящий вариант разобран на странице рекурсия с мемоизацией.

Вопросы и ответы

Как понять, что задача не на жадный алгоритм?

Найдите пример, где лучший локальный выбор портит итог. Монеты [1, 3, 4] и сумма 6 такой пример. Нашли, значит нужна таблица.

Сколько состояний может быть?

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

Что делать, если нужен сам ответ, а не число?

Храните рядом с dp[i] выбор, который к нему привёл. В конце пройдите по выборам назад и соберите ответ.

Где динамическое программирование встречается в реальных системах?

В выравнивании ДНК и в проверке орфографии через расстояние редактирования. В сжатии данных и в разбиении текста на строки в вёрстке. В маршрутизации, где ищут дешёвый путь по этапам.