Взрыв шариков: интервальный DP в обратном порядке
Решите задачу о взрыве шариков, рассуждая в обратном порядке: выбирайте последний шарик для взрыва в каждом интервале, а не первый
«Взрыв шариков: интервальный DP в обратном порядке» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.
Задача о лопающихся шарах
Даны n шаров со значениями nums. При лопании шара i вы получаете nums[i-1] * nums[i] * nums[i+1] монет — произведение значения самого шара и его текущих соседей. После лопания шара его соседи становятся соседними друг с другом. Найдите максимальное число монет, которое можно получить, лопнув все шары. Наивное моделирование сложно, поскольку при лопании меняются соседи, — обратное интервальное DP элегантно обходит эту трудность.
Почему прямая симуляция не работает
Если попытаться определить dp[i][j] как максимальное число монет при лопании шаров в диапазоне [i, j] и рассуждать о том, какой шар лопнуть первым, возникает проблема: если первым лопнуть шар k, то nums[k-1] и nums[k+1] должны быть его текущими соседями, но эти шары могут лопнуть позже, из-за чего соседство изменится. Состояние трудно чётко определить при движении вперёд.
Главная идея: рассуждайте в обратном порядке
Хитрость заключается в том, чтобы рассмотреть, какой шар лопается последним в интервале [i, j]. Когда шар k лопается последним в [i, j], все остальные шары в [i, j] уже исчезли. Поэтому соседями шара k будут ровно nums[i-1] и nums[j+1] — граничные шары непосредственно за пределами интервала. Благодаря этому вычисление монет за последнее лопание становится однозначным: оно не зависит от порядка предыдущих лопаний.
Определение состояния и рекуррентного соотношения
Добавьте фиктивные шары: вставьте 1 в начало и конец nums, чтобы получить nums = [1] + nums + [1]. Определите dp[i][j] как максимальное число монет при лопании всех шаров строго между индексами i и j (исключая границы), где nums[i] и nums[j] — оставшиеся граничные шары. Рекуррентное соотношение: для каждого возможного последнего шара k в (i, j): dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j]).
# With sentinels: nums = [1] + original + [1]
# dp[i][j] = max coins from bursting all balloons in open interval (i, j)
# k = last balloon to burst in (i,j)
# dp[i][j] = max over k in (i,j): dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j]Полная реализация
Мы дополняем массив фиктивными шарами, инициализируем таблицу DP нулями (пустой интервал = 0 монет) и заполняем её при возрастании длины интервала. Итоговый ответ — dp[0][n+1], представляющий максимальное число монет при лопании всех исходных шаров с фиктивными шарами в качестве постоянных границ.
def maxCoins(nums):
nums = [1] + nums + [1]
n = len(nums)
dp = [[0]*n for _ in range(n)]
# length of open interval (i, j) exclusive: j - i - 1 balloons inside
for length in range(2, n): # length = j - i
for i in range(0, n - length):
j = i + length
for k in range(i+1, j): # k is last burst in (i, j)
coins = dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j]
dp[i][j] = max(dp[i][j], coins)
return dp[0][n-1]
print(maxCoins([3, 1, 5, 8])) # 167Пошаговый разбор примера
Для [3, 1, 5, 8] после добавления фиктивных границ получаем [1, 3, 1, 5, 8, 1] (индексы 0–5). Нас интересует dp[0][5]. Для интервалов длины 2 (внутри находится один шар): dp[0][2] = 1*3*1=3, dp[1][3]=3*1*5=15, dp[2][4]=1*5*8=40, dp[3][5]=5*8*1=40. Постепенно заполняя таблицу, получаем оптимальный вариант: лопнуть 1 последним среди {3,1,5,8}, предварительно лопнув соседей, что даёт всего 167 монет.
Анализ сложности
Существует O(n²) интервалов, и для каждого интервала мы перебираем O(n) точек разделения, поэтому получаем временную сложность O(n³). Объём памяти для таблицы DP составляет O(n²). Для n = 500 шаров это 125 миллионов операций — приемлемо для ограничений на собеседовании. Добавление фиктивных границ упрощает обработку границ: без него потребовались бы явные проверки того, находятся ли i-1 и j+1 в допустимом диапазоне.
Нисходящая альтернатива с мемоизацией
То же решение можно записать в нисходящем стиле с помощью @lru_cache; на собеседовании такой вариант может быть интуитивно понятнее при выводе решения. Определите solve(i, j) как максимальное число монет в открытом интервале (i, j). Функция перебирает все варианты k как последнего лопнувшего шара и сохраняет результаты в кэше. Оба подхода имеют одинаковую сложность по времени и памяти.
from functools import lru_cache
def maxCoins_memo(nums):
nums = [1] + nums + [1]
n = len(nums)
@lru_cache(maxsize=None)
def solve(i, j):
if j - i < 2: # no balloons between i and j
return 0
return max(
solve(i, k) + solve(k, j) + nums[i]*nums[k]*nums[j]
for k in range(i+1, j)
)
return solve(0, n-1)
print(maxCoins_memo([3, 1, 5, 8])) # 167Распространённая ошибка: определение прямого DP
Распространённая ошибка — определить dp[i][j] как число монет при лопании первого шара в [i,j], а не последнего. Это не работает, поскольку вычисление монет за первое лопание зависит от соседних шаров, которые ещё не лопнули, а состояние этих соседей меняется по мере выполнения алгоритма. При интервальном DP всегда рассматривайте последний элемент, если границы зависят от оставшихся элементов.
Почему фиктивные значения равны 1
Фиктивные значения 1 выбираются потому, что они выступают в роли нейтральных элементов при умножении. Когда граничный шар лопается последним, количество монет равно boundary * last * boundary = 1 * last * 1 = last. Использование 0 дало бы 0 монет, что неверно, а другие значения исказили бы вычисление. Приём с фиктивными значениями единообразно обрабатывает все случаи на границах без отдельных условий для самого левого и самого правого шаров.
Сравнение со стандартным интервальным DP
В стандартном интервальном DP (задаче о цепном умножении матриц) точка разделения k показывает, где мы делим задачу на две подзадачи, решаемые независимо. В задаче о лопающихся шарах k — это шар, который лопается последним в интервале, поэтому два подинтервала [i,k] и [k,j] становятся независимыми, если считать k всё ещё присутствующим в качестве границы. Именно этот обратный взгляд — ключевая идея, благодаря которой задачу о лопающихся шарах можно решить с помощью интервального DP.
Быстрая проверка
Проверьте, насколько вы поняли концепции «Структуры данных и алгоритмы — подготовка к собеседованию по программированию», изученные на этом уроке.
Итоги урока
В этом уроке Вы узнали: прямая симуляция не работает, потому что лопание шаров непредсказуемо изменяет соседние элементы, обратный подход определяет k как последний шар, лопнувший в интервале, поэтому соседями становятся nums[i] и nums[j], и рекуррентное соотношение dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j]) с дополнением фиктивными граничными элементами даёт решение за O(n³). Далее мы переходим к DP для задачи о рюкзаке, начиная с классической задачи о рюкзаке 0/1 и её оптимизации по памяти.
Часто задаваемые вопросы
Урок «Взрыв шариков: интервальный DP в обратном порядке» бесплатный?
Да — полный текст урока «Взрыв шариков: интервальный DP в обратном порядке» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Взрыв шариков: интервальный DP в обратном порядке»?
Решите задачу о взрыве шариков, рассуждая в обратном порядке: выбирайте последний шарик для взрыва в каждом интервале, а не первый Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Взрыв шариков: интервальный DP в обратном порядке»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Шаблон интервального DP и порядок заполнения
- Наибольшая палиндромная подпоследовательность и подстрока
- Разбиение палиндрома II
- Взрыв шариков: интервальный DP в обратном порядке