0Pricing
DSA Interview Prep · Урок

Шаблон интервального DP и порядок заполнения

Определите состояние интервального DP dp[i][j], объясните, почему интервалы нужно заполнять в порядке возрастания длины, и проследите этот шаблон на задаче перемножения цепочки матриц

«Шаблон интервального DP и порядок заполнения» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA Interview Prep содержит 4 уроков всего.

Что такое DP по интервалам

DP по интервалам — это шаблон динамического программирования, в котором состояние dp[i][j] представляет оптимальный ответ для подзадачи, охватывающей индексы i–j. Основная идея заключается в том, что сначала решаются меньшие интервалы, а затем постепенно строится решение для всего диапазона. Этот шаблон естественным образом описывает такие задачи, как умножение цепочки матриц, разбиение на палиндромы и лопание шаров, где границами подзадачи являются левый и правый концы диапазона.

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

В DP по интервалам состоянием является dp[i][j], где i <= j. Базовые случаи — это интервалы из одного элемента: dp[i][i]. Они решаются очевидным образом — например, для одной матрицы стоимость умножения равна нулю. Для интервалов из двух элементов dp[i][i+1] также часто существуют простые решения. Мы заполняем таблицу для интервалов возрастающей длины, начиная с длины 1 и заканчивая n.

n = 4
dp = [[0] * n for _ in range(n)]
# Base cases: single elements
for i in range(n):
    dp[i][i] = 0  # length-1 intervals

Порядок заполнения: увеличение длины

Ключевая деталь DP по интервалам — это порядок заполнения. Необходимо вычислить все интервалы длины L до вычисления интервалов длины L+1, поскольку более длинный интервал зависит от более коротких подинтервалов. Внешний цикл перебирает длину интервала от 2 до n, средний цикл задаёт левую границу i, а правую границу мы вычисляем как j = i + L - 1.

n = 5
dp = [[float('inf')] * n for _ in range(n)]
for i in range(n):
    dp[i][i] = 0

for length in range(2, n + 1):      # interval length
    for i in range(n - length + 1): # left boundary
        j = i + length - 1          # right boundary
        for k in range(i, j):       # split point
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j])

Настройка умножения цепочки матриц

Классическая задача на DP по интервалам — умножение цепочки матриц: по заданным матрицам с размерами dims[0..n] найдите минимальное число скалярных умножений, необходимое для вычисления произведения. Умножение матрицы A(p×q) на B(q×r) требует p*q*r операций. dp[i][j] = минимальная стоимость умножения матриц от i до j. Точка разделения k определяет, где последовательность разделяется на две подцепочки.

def matrix_chain_order(dims):
    n = len(dims) - 1  # number of matrices
    dp = [[0] * n for _ in range(n)]
    
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            dp[i][j] = float('inf')
            for k in range(i, j):
                cost = dp[i][k] + dp[k+1][j] + dims[i]*dims[k+1]*dims[j+1]
                dp[i][j] = min(dp[i][j], cost)
    return dp[0][n-1]

print(matrix_chain_order([10, 30, 5, 60]))  # 4500

Разбор таблицы DP

Разберём пример с цепочкой матриц с размерами [10, 30, 5, 60], представляющих три матрицы: A(10×30), B(30×5), C(5×60). Для dp[0][2] проверим разделение в точке k=0: dp[0][0] + dp[1][2] + 10×30×60 = 0 + 9000 + 18000 = 27000, а также в точке k=1: dp[0][1] + dp[2][2] + 10×5×60 = 1500 + 0 + 3000 = 4500. Таким образом, dp[0][2] = 4500; этот результат достигается, если сначала перемножить AB.

Почему этот порядок заполнения работает

При вычислении dp[i][j] мы обращаемся к dp[i][k] и dp[k+1][j] для всех k из [i, j-1]. Оба подинтервала имеют строго меньшую длину, чем [i, j]. Если перебирать длину от меньшей к большей, все необходимые подинтервалы будут вычислены до того, как понадобятся. Это основное доказательство корректности порядка заполнения DP по интервалам: более короткие интервалы всегда являются зависимостями более длинных.

DP по интервалам сверху вниз с мемоизацией

Кроме того, DP по интервалам можно реализовать сверху вниз с мемоизацией. Мы пишем рекурсивную функцию solve(i, j), которая возвращает оптимальную стоимость для интервала [i, j], и сохраняем результаты в словаре. Порядок заполнения автоматически обрабатывается рекурсией. Подход сверху вниз часто проще для понимания, но может иметь накладные расходы на вызовы функций; подход снизу вверх на практике работает быстрее для больших входных данных.

from functools import lru_cache

def matrix_chain_memo(dims):
    n = len(dims) - 1
    
    @lru_cache(maxsize=None)
    def solve(i, j):
        if i == j:
            return 0
        return min(
            solve(i, k) + solve(k+1, j) + dims[i]*dims[k+1]*dims[j+1]
            for k in range(i, j)
        )
    
    return solve(0, n-1)

print(matrix_chain_memo([10, 30, 5, 60]))  # 4500

Сложность по времени и памяти

DP по интервалам имеет O(n²) состояний — все пары (i, j) — и каждое состояние перебирает O(n) точек разделения, что в сумме даёт время O(n³). Для таблицы DP требуется память O(n²). Для умножения цепочки из 100 матриц это 1 000 000 операций — вполне приемлемый объём. Этот шаблон встречается во многих сложных задачах LeetCode и часто используется на собеседованиях FAANG благодаря своей неочевидной структуре.

Восстановление оптимального решения

Чтобы восстановить фактическую расстановку скобок, а не только стоимость, храните отдельную таблицу split[i][j], в которой для каждого состояния записывается значение k, достигшее минимума. Затем рекурсивно считайте точки разделения: reconstruct(i, j) выводит оптимальную группировку, рекурсивно обрабатывая [i, split[i][j]] и [split[i][j]+1, j]. Этот метод применим ко всем задачам на DP по интервалам.

def matrix_chain_with_split(dims):
    n = len(dims) - 1
    dp = [[0]*n for _ in range(n)]
    split = [[0]*n for _ in range(n)]
    
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            dp[i][j] = float('inf')
            for k in range(i, j):
                cost = dp[i][k] + dp[k+1][j] + dims[i]*dims[k+1]*dims[j+1]
                if cost < dp[i][j]:
                    dp[i][j] = cost
                    split[i][j] = k
    return dp[0][n-1], split

Шаблон для любой задачи на DP по интервалам

Универсальный шаблон DP по интервалам состоит из трёх частей: (1) инициализировать базовые случаи для отдельных элементов, (2) перебирать возрастающие длины и для каждой длины перебирать допустимые левые границы, вычисляя правую границу, и (3) для каждого интервала перебирать все точки разделения и применять рекуррентное соотношение, зависящее от задачи. Между задачами меняется только формула рекуррентного соотношения во внутреннем цикле.

def interval_dp_template(n, base_cost, split_cost):
    dp = [[float('inf')] * n for _ in range(n)]
    for i in range(n):
        dp[i][i] = base_cost(i)  # problem-specific base case
    
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            for k in range(i, j):
                # problem-specific recurrence
                candidate = dp[i][k] + dp[k+1][j] + split_cost(i, k, j)
                dp[i][j] = min(dp[i][j], candidate)
    
    return dp[0][n-1]

Распространённые задачи на DP по интервалам

DP по интервалам используется в таких задачах, как: Умножение цепочки матриц (минимизация числа операций), Взрывание шаров (максимизация количества монет), Странный принтер (минимизация операций печати), Триангуляция многоугольника с минимальной стоимостью и Разбиение на палиндромы II. В каждой используется один и тот же каркас порядка заполнения, но разные рекуррентные соотношения. Распознавайте этот шаблон, когда задача требует найти оптимальное значение для диапазона или последовательности, которую можно разделить в любой внутренней точке.

Быстрая проверка

Проверьте своё понимание понятий из курса «Структуры данных & алгоритмы — подготовка к собеседованию по программированию», рассмотренных в этом уроке.

Итоги урока

В этом уроке Вы узнали, что DP по интервалам использует dp[i][j] для представления оптимального ответа на диапазоне, порядок заполнения должен следовать возрастающей длине интервалов, чтобы подинтервалы вычислялись первыми, а универсальный шаблон требует времени O(n³) и памяти O(n²). Далее мы рассмотрим самую длинную палиндромную подпоследовательность и подстроку, используя этот же шаблон.

Часто задаваемые вопросы

Урок «Шаблон интервального DP и порядок заполнения» бесплатный?

Да — полный текст урока «Шаблон интервального DP и порядок заполнения» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Шаблон интервального DP и порядок заполнения»?

Определите состояние интервального DP dp[i][j], объясните, почему интервалы нужно заполнять в порядке возрастания длины, и проследите этот шаблон на задаче перемножения цепочки матриц Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать DSA Interview Prep?

Предыдущий опыт не требуется. DSA Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «Шаблон интервального DP и порядок заполнения»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке DSA Interview Prep?

Да. Каждый урок DSA Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Шаблон интервального DP и порядок заполнения
  2. Наибольшая палиндромная подпоследовательность и подстрока
  3. Разбиение палиндрома II
  4. Взрыв шариков: интервальный DP в обратном порядке
← Назад к DSA Interview Prep