Шаблон интервального 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 — локальная установка не требуется.
Все уроки этого курса
- Шаблон интервального DP и порядок заполнения
- Наибольшая палиндромная подпоследовательность и подстрока
- Разбиение палиндрома II
- Взрыв шариков: интервальный DP в обратном порядке