0Pricing
DSA Interview Prep · Урок

Взрыв шариков: интервальный DP в обратном порядке

Решите задачу о взрыве шариков, рассуждая в обратном порядке: выбирайте последний шарик для взрыва в каждом интервале, а не первый

«Взрыв шариков: интервальный DP в обратном порядке» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA 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) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Взрыв шариков: интервальный DP в обратном порядке»?

Решите задачу о взрыве шариков, рассуждая в обратном порядке: выбирайте последний шарик для взрыва в каждом интервале, а не первый Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

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

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

Сколько времени занимает урок «Взрыв шариков: интервальный DP в обратном порядке»?

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

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

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

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

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