312. Burst Balloons - Explanation

Problem Link

Description

You are given an array of integers nums of size n. The ith element represents a balloon with an integer value of nums[i]. You must burst all of the balloons.

If you burst the ith balloon, you will receive nums[i - 1] * nums[i] * nums[i + 1] coins. If i - 1 or i + 1 goes out of bounds of the array, then assume the out of bounds value is 1.

Return the maximum number of coins you can receive by bursting all of the balloons.

Example 1:

Input: nums = [4,2,3,7]

Output: 143

Explanation:
nums = [4,2,3,7] --> [4,3,7] --> [4,7] --> [7] --> []
coins =  4*2*3    +   4*3*7   +  1*4*7  + 1*7*1 = 143

Constraints:

  • n == nums.length
  • 1 <= n <= 300
  • 0 <= nums[i] <= 100


Topics

Recommended Time & Space Complexity

You should aim for a solution with O(n ^ 3) time and O(n ^ 2) space, where n is the size of the input array.


Hint 1

Try to simulate the process recursively by passing the array to the recursive function. At each step, iterate through the array, pop an element, and recursively apply the same process to the two subarrays on both sides of the popped element, returning the maximum result from all recursive paths. This approach is exponential. Can you think of a way to optimize it? Maybe you should consider observing the subproblems instead of modifying the array.


Hint 2

Instead of passing the array, we can pass the range of indices l and r that need to be processed. We pad the input array with 1s on both sides for easier computation, but l and r represent the first and last indices of the original input array. Can you think of a reverse engineering approach for popping elements?


Hint 3

We determine the result by considering each element as the last one to be popped in the current range. For each element, we calculate its value by multiplying it with the elements at l - 1 and r + 1, then recursively solve the subproblems for the ranges (l, i - 1) and (i + 1, r), where i is the current element in the given range. Can you think of a way to optimize and avoid redundant calculations?


Hint 4

We can use memoization to cache the results of recursive calls and avoid redundant calculations. A hash map or a 2D array can be used to store results since the recursive function parameters l and r are within the range of the input array size.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Recursion - Breaking down problems into smaller subproblems
  • Dynamic Programming - Memoization and tabulation techniques for optimization
  • Interval DP - Solving problems by considering all possible subranges and combining results
  • Array Manipulation - Padding arrays with boundary values to simplify edge cases

1. Brute Force (Recursion)

Intuition

This problem asks us to find the maximum coins we can collect by bursting balloons in the best possible order.

When we burst a balloon, the number of coins we gain depends on its current neighbors. Since bursting one balloon changes the neighbors of others, the order of bursting matters a lot.

A brute-force way to solve this is to try every possible balloon as the last one to burst in the current array:

  • If we choose a balloon to burst now, we earn coins based on its left and right neighbors.
  • Then the problem reduces to bursting the remaining balloons.

The recursive function represents:
“What is the maximum number of coins we can collect from the current list of balloons?”

To simplify edge cases, we add 1 to both ends of the array so every balloon always has two neighbors.

Algorithm

  1. Add 1 to the beginning and end of the array to handle boundaries safely.
  2. Define a recursive function dfs(nums):
    • nums represents the current list of remaining balloons
  3. If only the two boundary 1s are left:
    • Return 0 since no real balloons remain
  4. Initialize maxCoins = 0
  5. For each balloon index i between the boundaries:
    • Calculate coins gained by bursting nums[i]:
      • nums[i - 1] * nums[i] * nums[i + 1]
    • Recursively compute the maximum coins from the remaining balloons:
      • Remove nums[i] and call dfs on the new list
    • Update maxCoins with the best result
  6. Return maxCoins for the current configuration
  7. Start the recursion with the full padded array and return the result
class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        nums = [1] + nums + [1]

        def dfs(nums):
            if len(nums) == 2:
                return 0

            maxCoins = 0
            for i in range(1, len(nums) - 1):
                coins = nums[i - 1] * nums[i] * nums[i + 1]
                coins += dfs(nums[:i] + nums[i + 1:])
                maxCoins = max(maxCoins, coins)
            return maxCoins

        return dfs(nums)

Time & Space Complexity

  • Time complexity: O(n2n)O(n*2^n)
  • Space complexity: O(n2n)O(n*2^n)

2. Dynamic Programming (Top-Down)

Intuition

The brute-force recursion tries every possible bursting order, which repeats the same work many times.

A useful way to think about this problem is:
instead of choosing the first balloon to burst, choose the last balloon to burst in a subarray.

Why this helps:

  • If balloon i is the last one to burst between indices l and r, then at that moment:
    • everything inside (l..r) except i is already gone
    • the neighbors of i are fixed: nums[l - 1] on the left and nums[r + 1] on the right
  • So the coins gained from bursting i last are:
    • nums[l - 1] * nums[i] * nums[r + 1]
  • And the remaining work splits cleanly into two independent parts:
    • best coins from (l..i-1)
    • best coins from (i+1..r)

This creates overlapping subproblems, so we store results in a memo table dp keyed by (l, r).

Algorithm

  1. Add 1 to both ends of the array to avoid boundary checks.
  2. Use a memo table dp where:
    • dp[(l, r)] stores the maximum coins obtainable by bursting balloons in the range [l, r]
  3. Define a recursive function dfs(l, r):
    • If l > r, return 0 (no balloons to burst)
    • If (l, r) is already computed, return it
  4. Try every balloon i in [l, r] as the last balloon to burst:
    • Coins from bursting i last:
      • nums[l - 1] * nums[i] * nums[r + 1]
    • Plus the best coins from the left side:
      • dfs(l, i - 1)
    • Plus the best coins from the right side:
      • dfs(i + 1, r)
    • Take the maximum over all choices of i
  5. Store the best value in dp[(l, r)] and return it
  6. The final answer is dfs(1, len(nums) - 2) (the original balloons, excluding the added boundaries)
class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        nums = [1] + nums + [1]
        dp = {}
        def dfs(l, r):
            if l > r:
                return 0
            if (l, r) in dp:
                return dp[(l, r)]

            dp[(l, r)] = 0
            for i in range(l, r + 1):
                coins = nums[l - 1] * nums[i] * nums[r + 1]
                coins += dfs(l, i - 1) + dfs(i + 1, r)
                dp[(l, r)] = max(dp[(l, r)], coins)
            return dp[(l, r)]

        return dfs(1, len(nums) - 2)

Time & Space Complexity

  • Time complexity: O(n3)O(n ^ 3)
  • Space complexity: O(n2)O(n ^ 2)

3. Dynamic Programming (Bottom-Up)

Intuition

We want the maximum coins we can get by bursting balloons in the best order.
The key trick is to think in reverse:

Instead of choosing which balloon to burst first, we choose which balloon is burst last in a range.

If we are only looking at balloons between l and r, and balloon i is the last one burst in that range, then:

  • all balloons inside (l..r) except i are already gone
  • so the neighbors of i are fixed as new_nums[l - 1] and new_nums[r + 1]
  • coins gained from bursting i last are:
    • new_nums[l - 1] * new_nums[i] * new_nums[r + 1]

After bursting i last, the remaining problem splits into two independent subproblems:

  • best coins for [l .. i-1]
  • best coins for [i+1 .. r]

This makes the problem perfect for interval DP.

Algorithm

  1. Create a new array new_nums = [1] + nums + [1] to handle boundaries easily.
  2. Let dp[l][r] represent the maximum coins we can collect by bursting balloons from index l to r (inside new_nums).
  3. Initialize dp with zeros since empty ranges give 0 coins.
  4. Fill the DP table by increasing interval size:
    • iterate l from n down to 1
    • iterate r from l up to n
  5. For each interval [l, r], try every balloon i in [l, r] as the last balloon to burst:
    • coins from bursting i last:
      • new_nums[l - 1] * new_nums[i] * new_nums[r + 1]
    • plus the best coins from the left sub-interval:
      • dp[l][i - 1]
    • plus the best coins from the right sub-interval:
      • dp[i + 1][r]
    • take the maximum over all i
  6. The final answer is stored in dp[1][n], representing bursting all original balloons.
class Solution:
    def maxCoins(self, nums):
        n = len(nums)
        new_nums = [1] + nums + [1]

        dp = [[0] * (n + 2) for _ in range(n + 2)]
        for l in range(n, 0, -1):
            for r in range(l, n + 1):
                for i in range(l, r + 1):
                    coins = new_nums[l - 1] * new_nums[i] * new_nums[r + 1]
                    coins += dp[l][i - 1] + dp[i + 1][r]
                    dp[l][r] = max(dp[l][r], coins)

        return dp[1][n]

Time & Space Complexity

  • Time complexity: O(n3)O(n^3)
  • Space complexity: O(n2)O(n^2)

Common Pitfalls

Thinking About First Balloon to Burst Instead of Last

The key insight is to consider which balloon is burst LAST in a range, not first. When thinking about the first balloon, the subproblems are not independent because neighbors change. When considering the last balloon, the boundary neighbors are fixed.

# Wrong mental model: burst balloon i first
# The remaining array's structure changes unpredictably

# Correct: burst balloon i last in range [l, r]
# Neighbors of i are fixed as nums[l-1] and nums[r+1]

Forgetting to Pad the Array with 1s

The problem states that out-of-bounds neighbors are treated as value 1. Forgetting to add 1 at both ends of the array causes index errors or incorrect coin calculations at the boundaries.

# Wrong: using original array
nums = [3, 1, 5, 8]

# Correct: pad with 1s
nums = [1] + nums + [1]  # [1, 3, 1, 5, 8, 1]

Incorrect Loop Order in Bottom-Up DP

In bottom-up DP, smaller intervals must be computed before larger ones. The left index l should iterate in decreasing order while r iterates in increasing order. Wrong iteration order reads uncomputed DP values.

Using Wrong Neighbors in Coin Calculation

When balloon i is the last to burst in range [l, r], its neighbors are nums[l-1] and nums[r+1] (the boundaries), not nums[i-1] and nums[i+1] (adjacent indices).

# Wrong: using adjacent indices
coins = nums[i-1] * nums[i] * nums[i+1]

# Correct: using range boundaries
coins = nums[l-1] * nums[i] * nums[r+1]

Not Handling Empty Range Base Case

When l > r, there are no balloons to burst, so the result should be 0. Missing this base case causes incorrect recursion or array index errors.