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

Самостоятельно строим состояние, базу и переход. От повторных вычислений к маршрутам, восстановлению ответа и таблицам; матрицы — дополнительное исследование.

01

Фибоначчи: цена повторных вычислений

Вычислим F(5): F(4)+F(3). Для F(4) снова нужны F(3)+F(2), так что F(3) решается дважды. Его ветви тоже повторяются. На рисунке нажмите F(3), чтобы увидеть одинаковые подзадачи. Наша цель — выполнять работу для каждого аргумента один раз.

Динамическое программирование (Dynamic programming) — Метод решения задачи через сохранение ответов на повторяющиеся подзадачи и переходы между ними.

Карта решения: к этим вопросам возвращайтесь в каждой новой задаче на динамическое программирование.

Наивная рекурсия F(n) = F(n−1) + F(n−2) повторно вычисляет одни и те же подзадачи. Дерево вызовов разрастается экспоненциально.

F(0)=0; F(1)=1; F(n)=F(n−1)+F(n−2) при n≥2

Это соглашение используется в тексте, моделях и коде всей основной части.

Разные вызовы и разные состояния

Во всей основной части F(0)=0, F(1)=1. Для F(5) получаем F(4)+F(3), а F(4) снова вызывает F(3). Нажмите F(k): все повторения того же аргумента выделятся.

F(n) = 5
Наивных вызовов: 15
Разных состояний в дереве: 6
F(0): 3 выз.[0]
F(1): 5 выз.[1]
F(2): 3 выз.[2]
F(3): 2 выз.[3]
F(4): 1 выз.[4]
F(5): 1 выз.[5]

Сколько вызовов F(3) содержит наивное вычисление F(5)?

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

Наивная рекурсия
fib(n):
    if n < 2: return n
    return fib(n-1) + fib(n-2)

Многократно выполняются вызовы с одинаковым аргументом.

Главная идеяЕсли состояние зависит только от n, запоминать вычисленный F(n) достаточно один раз.

Для F(5) дерево содержит 15 вызовов, но всего 6 разных аргументов: 0…5. Три вызова F(2) решают одну и ту же подзадачу. Запоминание ответов сокращает повторную работу. Теперь, после примера, соберём вопросы, с которых будем начинать собственное ДП. В задачах о путях база будет другой: её нельзя копировать из Фибоначчи.

Шаг проектированияЧто нужно сформулировать
СостояниеКакую подзадачу обозначает d[i] или d[i][j]; достаточно ли параметров?
БазаГде ответ известен без переходов; чем обозначена недостижимость?
ПереходКакие предшественники нужны; складываем количества или выбираем оптимум?
ПорядокВсе зависимости вычислены раньше текущего состояния.
ОценкаЧисло состояний × работа переходов; дополнительно память, стек и размер чисел.
ВосстановлениеНужен ли сам путь и какие данные о предке следует сохранить?

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Рекурсивный перебор одинаковых префиксов

Для n = 40 наивный Фибоначчи уже делает сотни миллионов вызовов. Уникальных аргументов около сорока: разрыв показывает, что кэшировать нужно состояние, а не пытаться ускорять один вызов.

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

Посчитайте повторы до выбора способа ускорения

Задания раздела

Для F(5) с базой F(0)=0,F(1)=1 сколько раз наивная рекурсия вызывает F(3)?

В полном дереве F(5) сколько разных аргументов?

Попыток: 0
02

Мемоизация сверху вниз

Этот раздел откроется после предыдущих проверок.

Продолжить: Фибоначчи: цена повторных вычислений →

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

Мемоизация (Memoization) — Кэширование результатов рекурсивных подзадач при первом обращении.

Перед рекурсивным вычислением fib(n) проверяем кэш: если ответ есть, возвращаем его сразу. Так получаем O(n) состояний вместо экспоненциального числа рекурсивных вызовов.

Фибоначчи с мемоизацией
memo = array(n+1, UNSET)
fib(k):
    if memo[k] != UNSET: return memo[k]
    if k < 2: ans = k
    else: ans = fib(k-1) + fib(k-2)
    memo[k] = ans
    return ans

O(n) времени, O(n) памяти на кэш и стек вызовов.

То же F(5), но с кэшем

Базовые F(0), F(1) уже в кэше. Сравните первое и повторное обращение к F(3).

0[0]
1[1]
?[2]
?[3]
?[4]
?[5]

→ вычисление / сохранение

F(5): cache miss — ответа нет, раскрываем рекурсию.
Шаг 1 / 13

Левая ветвь уже сохранила F(3). Правая ветвь снова просит F(3). Выполнятся ли вызовы F(2) и F(1)?

Это top-down DP: дерево рекурсивных запросов вычисляет только действительно нужные состояния и больше не пересчитывает их.

Пограничные случаи
База F(0)=0, F(1)=1 обрабатывает n=0 без отрицательных индексов. Значение 0 — корректный результат, поэтому отсутствие в кэше обозначено UNSET, а не нулём.

Ключ кэша должен полностью описывать подзадачу: одного номера клетки не хватит, если ещё важен оставшийся ресурс. Отсутствующий результат лучше отмечать None/Option или отдельным флагом, поскольку ноль может быть корректным ответом. Кэш не устраняет глубину рекурсии: цепочка из 200 000 состояний всё ещё опасна для стека Python.

Одни состояния: кэш или порядок вычисления

F(0)=0, F(1)=1. Мемоизация использует O(n) памяти и глубину рекурсии O(n); итеративный вариант — O(1) дополнительной памяти. Оба выполняют O(n) сложений. Для точного Rust-результата ограничиваем n ≤ 93, чтобы F(n) помещалось в u64; Python использует большие целые.

Одни состояния: кэш или порядок вычисления / Python
def fib_memo(n):
    memo = [None] * (n + 1)
    def solve(k):
        if memo[k] is not None: return memo[k]
        memo[k] = k if k < 2 else solve(k - 1) + solve(k - 2)
        return memo[k]
    return solve(n)

def fib_iter(n):
    if n == 0: return 0
    a, b = 0, 1
    for _ in range(1, n): a, b = b, a + b
    return b

print(fib_memo(10), fib_iter(10))
Ожидаемый вывод
55 55

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Задача с ресурсом

Если путь зависит от клетки и остатка энергии, ключ состояния должен содержать оба параметра. Кэш только по клетке смешает разные подзадачи и даст неверный ответ даже при быстром выполнении.

Каждое состояние теперь вычисляется один раз. Попробуем задать порядок вычислений явно и уменьшить память.

Предскажите cache hit и выберите ключ

Задания раздела

F(3) уже сохранено, снова вызвано fib(3). Что выполнится?

Ответ зависит от клетки и остатка энергии. Какие параметры входят в ключ?

Попыток: 0
03

Табуляция снизу вверх и уменьшение памяти

Этот раздел откроется после предыдущих проверок.

Продолжить: Фибоначчи: цена повторных вычислений →

Мемоизация сохраняет форму рекурсивного решения. Но если порядок зависимостей известен заранее, можно вообще отказаться от рекурсивного стека и заполнить ответы от маленьких состояний к большим.

Табуляция (Bottom-up DP) — Вычисление состояний в таком порядке, чтобы все зависимости уже были известны.

Сначала создадим dp[0…n]: dp[0]=0, при n≥1 dp[1]=1. Для i от 2 до n запишем dp[i]=dp[i−1]+dp[i−2]. На n=6 таблица получается [0,1,1,2,3,5,8]. Только после заполнения заметим, что для следующего значения нужны две последние ячейки.

Сначала таблица, затем две переменные
0[0]
?[1]
?[2]
?[3]
?[4]
?[5]
?[6]
Задаём базу F(0)=0.
Шаг 1 / 7

Уже вычислены dp[0…5]. Какие ячейки нужны для dp[6]?

Теперь a=F(4)=3, b=F(5)=5. Сначала new=a+b=8, затем a=b, b=new. Одновременное обновление предотвращает потерю прежнего b.

Итерация по двум переменным
if n == 0: return 0
a, b = 0, 1
for i = 2..n:
    a, b = b, a + b
return b

Сначала вычисляется новое значение, затем обновляются обе переменные.

МетодВремяПамять
Наивная рекурсияЭкспоненциальноеO(n) стек
МемоизацияO(n)O(n)
ТабуляцияO(n)O(n)
Две переменныеO(n)O(1)

Перед итерацией i переменные a и b должны хранить F(i−2) и F(i−1). Сначала считаем новое значение из старых, затем сдвигаем пару. Память O(1) достаточна для самого числа, но не для выдачи всех значений или восстановления маршрута. Оценка O(n) времени предполагает арифметику фиксированной стоимости; точные большие целые требуют учитывать длину чисел.

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Ответ без всей таблицы

Для Climbing Stairs достаточно двух предыдущих количеств. Но если условие требует вывести сами значения для всех ступеней, одно возвращаемое число уже не выражает нужный результат.

Механизм ДП понятен. Дальше научимся получать состояние и формулу из условия задачи, а не из готовой последовательности.

Заполните таблицу и сохраните нужную память

Задания раздела

dp=[0,1,1,2,3,5,?]. Введите dp[6].

Какие значения нужны для dp[7]?

Попыток: 0
04

Кенгуру: подсчёт путей

Этот раздел откроется после предыдущих проверок.

Продолжить: Фибоначчи: цена повторных вычислений →

Числа Фибоначчи были примером готовой формулы. Теперь выведем рекуррентность из условия задачи: кенгуру прыгает по клеткам на один или два шага. Начнём с точного смысла одного состояния.

Есть ряд клеток. Из каждой можно перепрыгнуть на одну или две клетки; задача — посчитать, сколькими способами можно попасть в последнюю. Каждый путь в i оканчивается прыжком из i−1 или из i−2.

d[i] = d[i−1] + d[i−2]

База d[0]=1: один пустой путь уже находится на старте. Отсутствующие индексы дают вклад 0.

Прыжки и запретные клетки

Нажмите на клетку, чтобы закрыть или открыть её.

Способов попасть в 7: 21. База d[0] = 1.

Если разрешены прыжки 1…k, формула обобщается до суммы k предыдущих состояний, что даёт прямую реализацию за O(nk). При необходимости скользящая сумма снижает время до O(n).

Что именно считается способом
Различными считаются последовательности длин прыжков. Для клеток 0→3 маршруты 1+1+1, 1+2 и 2+1 различны.

Пусть d[i] — число способов попасть ровно в i. В нулевой клетке есть один пустой маршрут, поэтому d[0]=1. Последний прыжок из i−1 и последний прыжок из i−2 образуют непересекающиеся классы путей — их количества складываются. Для запретной клетки ставим 0. При вычислении по модулю берём остаток после каждого сложения.

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Закрытые клетки и число маршрутов

Вариант с запретными клетками меняет базу или обнуляет конкретное состояние, но не требует нового рекурсивного перебора. Для прыжков на 1…k сначала оцените O(nk), затем подумайте о скользящей сумме.

Сложение объединило количества маршрутов. Для оптимизации заменим его минимумом и сохраним лучший переход.

Кенгуру стартует в 0, прыгает на 1 или 2 и должен попасть ровно в n. Клетки из blocked закрыты, в том числе может быть закрыт старт. Верните число путей. 0 ≤ n ≤ 30, все закрытые индексы лежат в [0,n].

Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

Открыто 0 из 3

    5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

    Тест 1 jump_ways(4, [])Ожидается: 5
    Тест 2 jump_ways(4, [2])Ожидается: 1

    Дополнительные тесты: 7. Проверяются границы и повторы.

    Попыток: 0
    05

    Оптимизация вместо подсчёта

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Фибоначчи: цена повторных вычислений →

    Вместо количества путей попросим самый дешёвый маршрут. Граф допустимых прыжков тот же; меняется операция, которая объединяет ответы предшественников.

    Если у каждой клетки есть цена c[i], надо найти маршрут с минимальной суммой цен. Вместо суммы количества способов используем минимум стоимости.

    d[i] = c[i] + min(d[i−1], d[i−2])

    d[i] — минимальная стоимость попадания в i при прыжках на 1 или 2.

    Стоимость и восстановление маршрута

    Для клетки 3 выберите лучшего предка. При равенстве выбираем меньший индекс.

    3[0]
    7[1]
    2[2]
    5[3]
    1[4]
    4[5]

    Оптимальные стоимости: 3, 10, 5, 10, 6, 10

    Ответ: 10. Маршрут: 0 → 2 → 4 → 5. Стартовая клетка оплачивается.

    Для восстановления ответа храним p[i] — предшествующую клетку, из которой получен оптимум. Начав с конечной клетки, переходим по p до начала и разворачиваем маршрут.

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

    В нашем примере старт находится в клетке 0 и его цена оплачивается: d[0]=c[0], d[1]=c[1]+d[0]. Это отличается от задач, разрешающих начать с клетки 1 бесплатно. Храним p[i], выбранного предшественника, затем идём от финиша до старта и разворачиваем путь. При равенстве стоимостей можно выбрать любого оптимального предка, если условие не требует особого порядка.

    Минимальная стоимость и маршрут

    Нужно дойти из клетки 0 в последнюю прыжками на 1 или 2. Цена старта включена. На каждой клетке сохраняем оптимального предка; равенство разрешаем в пользу меньшего индекса. Время O(n), память O(n) для восстановления. Пустой вход возвращает ноль и пустой маршрут. Для Rust предполагаем n ≤ 2·10⁵ и |c[i]| ≤ 10⁹.

    Минимальная стоимость и маршрут / Python
    def mincost_path(cost):
        if not cost: return 0, []
        dp, parent = cost.copy(), [-1] * len(cost)
        for i in range(1, len(cost)):
            p = i - 2 if i >= 2 and dp[i-2] <= dp[i-1] else i - 1
            dp[i], parent[i] = cost[i] + dp[p], p
        path, i = [], len(cost) - 1
        while i != -1:
            path.append(i)
            i = parent[i]
        return dp[-1], path[::-1]
    
    value, path = mincost_path([3, 7, 2, 5, 1, 4])
    print(value)
    print(*path)
    Ожидаемый вывод
    10
    0 2 4 5

    В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

    Не перепутайте оплату старта

    В LeetCode 746 можно начать с нулевой или первой ступени; пример лекции обязан стартовать в нуле и оплачивает его. Перед переносом рекуррентности выпишите базу конкретной задачи.

    Оптимальный маршрут восстановлен. Теперь отделим сам граф переходов от направления, в котором выполняются обновления.

    Стартуйте в клетке 0, оплачивая её цену, и дойдите в последнюю прыжками на 1 или 2. Верните минимальную сумму цен. Для пустого массива ответ 0. Цены могут быть отрицательными.

    Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

    Открыто 0 из 3

      5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

      Тест 1 min_cost([3, 7, 2, 5, 1, 4])Ожидается: 10
      Тест 2 min_cost([5])Ожидается: 5

      Дополнительные тесты: 5. Проверяются границы и повторы.

      Попыток: 0
      06

      Динамика назад и вперёд

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Фибоначчи: цена повторных вычислений →
      Одна задача: собираем вклады сзади
      d = array(n+1, 0)
      d[0] = 1
      for i = 1..n:
          d[i] = d[i-1]
          if i >= 2: d[i] += d[i-2]

      Смысл d[i] тот же: число способов дойти ровно до i. Заполнение слева направо.

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

      Переход состояния (Transition) — Правило, вычисляющее вклад других состояний в текущее или будущие состояния.

      Просмотр назад (gather): для текущего i перебрать все возможные состояния-предшественники и собрать ответ. Просмотр вперёд (scatter): для уже посчитанного i обновить все достижимые следующие состояния.

      Передача вклада вперёд
      1[0]
      0[1]
      0[2]
      0[3]
      0[4]
      0[5]
      0[6]
      Начало: один пустой путь в клетку 0.
      Шаг 1 / 8

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

      Подсчёт способов просмотром вперёд
      d = array(n, 0)
      d[0] = 1
      for i = 0..n-1:
          if i+1 < n: d[i+1] += d[i]
          if i+2 < n: d[i+2] += d[i]

      Каждый исходящий переход переносит число путей из i в получателя.

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

      В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

      Перенос способов по рёбрам

      В динамике вперёд каждое ребро обновляет ответ получателя. Если состояния обрабатываются в топологическом порядке, все нужные вклады приходят до обработки получателя.

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

      Согласуйте переход и порядок

      Задания раздела

      d[i] — число способов дойти до i прыжками 1 или 2. Чем заполнить d[i]?

      Граф переходов: 0→1, 0→2, 1→3, 2→3. Постройте порядок; среди независимых 1 и 2 выбирайте меньший номер.

      1. 0
      2. 3
      3. 2
      4. 1
      Попыток: 0
      07

      Динамика на таблице

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Фибоначчи: цена повторных вычислений →

      Добавим вторую координату. Робот идёт по таблице только вправо и вниз, а некоторые клетки закрыты. Номер состояния теперь пара (строка, столбец), но порядок рассуждения остаётся прежним.

      В двумерной решётке, где можно ходить только вправо и вниз, число способов попасть в клетку равно сумме способов попасть в её верхнего и левого соседей.

      Пути в таблице с препятствиями

      Начните с сетки 3×3 без препятствий: [i][j] — строка i и столбец j. Верхняя и левая границы получают по одному пути. Три ячейки скрыты: сначала вычислите их сами. Нажатие переключает препятствие; формула остаётся прежней.

      Введите dp[1][1] на текущем поле

      Введите dp[1][2] на текущем поле

      Введите dp[2][2] на текущем поле

      Маршрутов до правого нижнего угла: 6.

      d[i][j] = d[i−1][j] + d[i][j−1]

      Для незаблокированной клетки. Начало d[0][0]=1; за пределами поля значения считаются нулевыми.

      Обход строками сверху вниз и столбцами слева направо гарантирует, что оба предшественника уже известны. Время и память — O(rows · cols).

      Как улучшить память
      Если интересует только число путей, а переходы берут данные из текущей строки и предыдущей, часто достаточно одной строки длины cols.

      Закрытая стартовая клетка даёт ноль маршрутов, открытая таблица 1×1 — один. В строковом обходе вектор dp[j] до обновления хранит ответ сверху, а dp[j−1] — уже обновлённый ответ слева. Так память сокращается до O(cols), но восстановление конкретного пути потребует дополнительных данных или повторного вычисления.

      Маршруты в таблице с памятью на одну строку

      0 означает открытую клетку, 1 — препятствие. Идём вправо и вниз, ответ считаем по модулю 1 000 000 007. До обновления dp[j] содержит ответ сверху, dp[j−1] — ответ слева. Время O(rows·cols), память O(cols). Пустая таблица даёт ноль; закрытый старт тоже даёт ноль.

      Маршруты в таблице с памятью на одну строку / Python
      def grid_paths(blocked):
          if not blocked or not blocked[0]: return 0
          cols = len(blocked[0])
          dp = [0] * cols
          dp[0] = 1
          for row in blocked:
              for j in range(cols):
                  if row[j]: dp[j] = 0
                  elif j: dp[j] = (dp[j] + dp[j-1]) % 1_000_000_007
          return dp[-1]
      
      print(grid_paths([[0,0,0],[0,1,0],[0,0,0]]))
      Ожидаемый вывод
      2

      В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

      Количество или минимальная сумма

      LeetCode 62 считает маршруты, 64 оптимизирует стоимость. Смысл состояния и правило объединения различны: сложение количеств нельзя заменить минимумом без изменения базы и недостижимости.

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

      В таблице 0 означает открытую клетку, 1 — препятствие. Из левого верхнего угла ходите только вправо и вниз до правого нижнего. Верните число маршрутов по модулю 1_000_000_007. Пустая таблица даёт 0.

      Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

      Открыто 0 из 3

        5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

        Тест 1 grid_paths([[0, 0, 0], [0, 1, 0], [0, 0, 0]])Ожидается: 2
        Тест 2 grid_paths([[1]])Ожидается: 0

        Дополнительные тесты: 6. Проверяются границы и повторы.

        Попыток: 0
        А—Я

        Справочник терминов

        Справочник собирает определения из разобранных разделов. Используйте поиск, если нужно восстановить термин; затем переходите к оценке ограничений и задачам для самостоятельного решения.

        Общий справочник всех лекций ↗

        Найдено: 13 из 13

        Динамическое программирование / Dynamic programmingРешение через состояние, переход и сохранение результатов.
        Рекуррентность / RecurrenceРавенство, связывающее значения задачи на разных состояниях.
        Мемоизация / MemoizationЗапоминание ответов рекурсивных подзадач.
        Табуляция / TabulationЗаполнение таблицы состояний в правильном порядке.
        Top-down / Сверху внизВычисление состояний по рекурсивному запросу.
        Bottom-up / Снизу вверхВычисление состояния после зависимостей.
        Состояние / StateКомпактное описание подзадачи.
        Переход / TransitionПравило обновления ответа из других состояний.
        Оптимальный предок / PredecessorСостояние, откуда получен оптимальный переход.
        Просмотр вперёд / Forward DPПеренос вклада в будущие состояния.
        Просмотр назад / Backward DPСбор ответа из предыдущих состояний.
        Топологический порядок / Topological orderПорядок, в котором зависимости обработаны раньше потомков.
        Матричное возведение / Matrix exponentiationУскорение повторного линейного преобразования.
        08

        Ограничения и выбор алгоритма

        Этот раздел откроется после предыдущих проверок.

        Продолжить: Фибоначчи: цена повторных вычислений →

        Сначала подставьте максимальные размеры из условия в оценку времени и памяти. Оценка числа действий — ориентир: константы, язык и ввод проверяются отдельно.

        Бюджет состояний и памяти

        Состояний 1000000; с двумя предками — до 2000000 сложений. Плотная таблица i64: 8.00 МБ; одна строка: 0.008 МБ. Оценки для массива чисел фиксированной ширины; объекты и списки Python требуют дополнительной памяти.

        n=200 000, на состояние перебираем k=200 000 прыжков. Что проверить прежде всего?

        Число состояний × работа переходов

        Для линии с n = 200 000 и двумя предками получается O(n). Для n = 200 000 и k = 200 000 полный перебор k прыжков на каждом состоянии даст до 4·10¹⁰ переходов. Маленькое тело цикла не компенсирует такое число повторений.

        Таблица и память

        Для 1000×1000 клеток нужен миллион состояний. Один массив i64 занимает примерно 8 МБ, четыре таких массива — 32 МБ без дополнительного хранения поля. В Python список списков с отдельными int имеет гораздо больший расход. Если нужны только значения соседних строк, переходите к O(cols) памяти.

        В задаче Working out нужны ответы из четырёх направлений, поэтому одной строки на всё решение недостаточно. Сначала выпишите, какие промежуточные значения понадобятся на этапе соединения маршрутов.

        Разрядность, рекурсия и большое n

        Число путей растёт быстро. Если условие требует модуль, применяйте его к каждому обновлению; если точный ответ — оцените длину больших целых. Сумма цен до 10⁹ на пути из 200 000 клеток требует i64. Нельзя использовать 0 как недостижимость, когда ответ 0 допустим.

        Top-down с глубиной 200 000 опасен для Python. Bottom-up убирает этот риск. Для n до 10¹⁸ фиксированная матрица размера s требует O(s³ log n), но только при действительно одинаковом линейном переходе. Проверьте нулевой шаг, закрытый старт, одну клетку, равные оптимумы и отсутствие маршрута.

        Оцените число переходов и память

        Задания раздела

        500 состояний, по 20 переходов у каждого. Сколько переходов прямого алгоритма?

        Плотная таблица 1000×1000, по 8 байт на число. Сколько МБ при 1 МБ=1 000 000 байт, без накладных расходов?

        Top-down зависит по цепочке от 200 000 предыдущих состояний. Что ещё проверить?

        Попыток: 0
        09

        Практика: 3 уровня

        Этот раздел откроется после предыдущих проверок.

        Продолжить: Фибоначчи: цена повторных вычислений →

        Постройте своё DP: состояние → база → переход → код

        Кенгуру прыгает на 1 или 3 клетки, некоторые клетки закрыты. Нужно посчитать пути ровно из 0 в n. Сначала сформулируйте подзадачу. Следующий шаг откроется после правильного ответа на предыдущий; решения сохраняются вместе с прогрессом курса.

        Конструктор 1/4: определите состояние

        Задания раздела

        Кенгуру стартует в 0, прыгает на 1 или 3. Есть запрещённые клетки; нужно число путей ровно в n. Что означает dp[i]?

        Попыток: 0

        Шаг 2 откроется после предыдущего решения.

        Шаг 3 откроется после предыдущего решения.

        Шаг 4 откроется после предыдущего решения.

        Основное применение

        После конструктора решите Climbing Stairs и Min Cost Climbing Stairs: одинаковые направления прыжков дают разные смысл состояния и базу. Затем перенесите вывод на таблицу. Сложное испытание объединяет несколько направлений и остаётся дополнительным.

        Примените разобранные приёмы самостоятельно. Для каждой задачи сначала сформулируйте требования к ответу и предложите простое решение. Затем подставьте максимальные ограничения, оцените время и память и решите, какое улучшение требуется. Подсказки с идеей и оценкой раскрываются отдельно.

        Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.

        01

        Лёгкие

        Три задачи для закрепления основных приёмов.

        1. LeetCode · 509

          Fibonacci Number

          Задать базу и убрать повторные вычисления.

          Ограничения. 0 ≤ n ≤ 30; F(0)=0, F(1)=1.

          Идея и оценка сложности

          Используйте то же соглашение, что и в лекции: F(0)=0, F(1)=1. Две переменные дают O(n) времени и O(1) памяти, memo — O(n) памяти.

        2. LeetCode · 70

          Climbing Stairs

          Получить сумму способов по последнему шагу.

          Ограничения. От 1 до 45 ступеней.

          Идея и оценка сложности

          Один или два последних шага задают два непересекающихся класса. База для количества путей: один пустой путь в 0, а не F(0)=0. O(n) времени, O(1) памяти.

        3. Вывести базу из возможности выбрать старт.

          Ограничения. От 2 до 1000 цен, каждое значение до 999.

          Идея и оценка сложности

          Можно начать с 0 или 1, а финиш находится за последней ступенью. Сформулируйте, когда платится цена, и не копируйте базу примера лекции без изменений. O(n), O(1) памяти без восстановления.

        02

        Средние

        Две задачи на сочетание идей и анализ ограничений.

        1. LeetCode · 62

          Unique Paths

          Упорядочить двумерные состояния.

          Ограничения. Размеры от 1 до 100; ответ в тестах не больше 2·10⁹.

          Идея и оценка сложности

          Ответ в клетке — сумма сверху и слева. Начало имеет один путь, вне поля — ноль. Память можно сократить до одной строки. O(rows·cols) времени, O(cols) памяти.

        2. LeetCode · 64

          Minimum Path Sum

          Заменить подсчёт путей оптимизацией стоимости.

          Ограничения. До 200×200 клеток, цены от 0 до 200.

          Идея и оценка сложности

          Прибавляйте цену клетки к минимуму достижимых предшественников. Ноль — допустимая стоимость, поэтому не обозначайте им отсутствие пути. O(rows·cols); при восстановлении сохраните предков.

        03

        Тяжёлая

        Одна задача для переноса идеи на новую постановку.

        1. Codeforces · 429B

          Working out

          Сочетать четыре направления ДП и локальные варианты встречи.

          Ограничения. 3 ≤ n,m ≤ 1000; цены до 10⁵.

          Идея и оценка сложности

          Посчитайте максимальные суммы из четырёх углов в разрешённых направлениях. Для каждой внутренней клетки проверьте два способа провести маршруты вокруг единственной встречи; сама клетка встречи не приносит цену. Итого O(nm) времени и четыре таблицы памяти. Уровень повышен в курсе из-за доказательства соединения маршрутов.

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

        Проверьте, что можете самостоятельно построить DP

        Задания раздела

        Соедините части конструкции и их роль.

        На том же поле задача изменилась: вместо числа путей ищем минимальную цену. Достаточно ли заменить знак + на min?

        Сформулируйте DP для прыжков 1 или 3 без просмотра готового текста.

        Попыток: 0
        Дополнительные материалыДля углублённого разбора · не влияют на прохождение
        ДОП.

        Линейные переходы и матричное ускорение

        Линейное ДП хорошо, когда число шагов умеренное. При n до 10¹⁸ последовательно пройти все состояния невозможно. Если переход постоянно задаётся одной небольшой матрицей, много одинаковых шагов можно объединить.

        Если переход имеет вид линейного преобразования фиксированного вектора состояния, его можно представить матрицей M и переходить сразу через много шагов как Mᵏ.

        Vₙ = MⁿV₀

        Степень матрицы вычисляется двоичным возведением за O(log n) умножений.

        Двоичная степень матрицы перехода

        n в двоичном виде: 10100. Все числа по модулю 1 000 000 007.

        109466765
        67654181

        F(20) = 6765. Матричных умножений: 6; последовательных шагов было бы 20.

        Основание: [1, 1; 1, 0].

        Накопитель: [1, 0; 0, 1].

        Старт: показатель 20, накопитель — единичная матрица.
        Шаг 1 / 10

        Для последовательности Фибоначчи достаточно матрицы 2×2. При постоянном числе состояний это уменьшает число шагов с O(n) до O(log n) при принятой модели стоимости арифметики.

        Ограничение методаПри большом числе состояний матричное умножение может оказаться существенно дороже исходных локальных переходов. Ускорение полезно лишь в подходящем режиме задачи.
        Цена больших чисел
        F(n) растёт экспоненциально по n, поэтому вычисления с неограниченной точностью требуют учитывать стоимость многобитной арифметики. O(log n) здесь относится к числу матричных умножений.

        Для Фибоначчи удобно Vₖ=(F(k+1),F(k)), M=[[1,1],[1,0]], V₀=(1,0). Тогда Vₙ=MⁿV₀. Умножение матриц размера s стоит O(s³), весь алгоритм — O(s³ log n), память O(s²). В примере считаем по модулю, чтобы не скрывать рост чисел. Произвольный нелинейный переход или меняющиеся по шагам условия не ускоряются одной фиксированной матрицей автоматически.

        Матричное ускорение одинакового перехода

        Фиксированная матрица 2×2 поднимается в степень двоичным методом. Получаем F(n) по модулю 1 000 000 007 за O(log(n+1)) умножений матриц и O(1) дополнительной памяти. При n ≤ 10¹⁸ степень помещается в u64; произведения двух остатков и их сумма тоже безопасны для u64.

        Матричное ускорение одинакового перехода / Python
        MOD = 1_000_000_007
        
        def multiply(a, b):
            return [[sum(a[i][k] * b[k][j] for k in range(2)) % MOD
                     for j in range(2)] for i in range(2)]
        
        def matrix_fib(n):
            answer, base = [[1,0],[0,1]], [[1,1],[1,0]]
            while n:
                if n & 1: answer = multiply(answer, base)
                base = multiply(base, base)
                n >>= 1
            return answer[1][0]
        
        print(matrix_fib(50))
        Ожидаемый вывод
        586268941

        В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

        Очень далёкий шаг

        Когда n достигает 10¹⁸, линейный проход невозможен даже при O(1) памяти. Для постоянного линейного перехода двоичная степень маленькой матрицы сокращает число этапов примерно до 60.

        Теперь пройдите весь путь решения: смысл состояния, база, переход, порядок, оценка и восстановление при необходимости. Практика ниже проверит каждый из этих шагов.

        Проверьте применимость матричного ускорения

        Задания раздела

        Какой переход допускает степень одной фиксированной матрицы?

        Показатель 13=1101₂. Сколько раз алгоритм умножает накопитель на основание (квадраты основания не считаем)?

        Попыток: 0

        ПРОДОЛЖИТЬ ОБСУЖДЕНИЕ

        Промпт для ИИ

        Скопируйте промпт с текстом лекции в чат с ИИ, чтобы обсудить непонятные места. В него входят дополнительные материалы, псевдокод и примеры на обоих языках.