LeetCode 312 - Burst Balloons
The problem gives us an array nums, where each element represents a balloon containing a number. When we burst a balloon at index i, we gain coins equal to: The important detail is that the neighbors of a balloon change dynamically as balloons are removed.
Difficulty: 🔴 Hard
Topics: Array, Dynamic Programming
Solution
LeetCode 312, Burst Balloons
Problem Understanding
The problem gives us an array nums, where each element represents a balloon containing a number. When we burst a balloon at index i, we gain coins equal to:
$\text{coins} = nums[i-1] \times nums[i] \times nums[i+1]$
The important detail is that the neighbors of a balloon change dynamically as balloons are removed. After a balloon is burst, it disappears, and the remaining balloons become adjacent.
If a balloon does not have a left or right neighbor because it is at the boundary, we treat the missing neighbor as having value 1.
The goal is to determine the maximum number of coins we can collect by choosing the best order to burst all balloons.
The input consists of:
- An integer array
nums nums[i]is the value painted on balloonincan be as large as300
The output is a single integer representing the maximum achievable coins.
The constraints are the key indicator that brute force will not work:
- There can be up to
300balloons - Every permutation of bursting order is potentially different
300!possible orders is astronomically large
This immediately suggests that we need a dynamic programming solution.
Several edge cases are important:
- Arrays of length
1 - Arrays containing zeros
- Arrays where all numbers are the same
- Cases where bursting a small balloon early enables larger multiplications later
- Boundary balloons, because they use virtual neighbors of value
1
A naive greedy approach can easily fail because local choices affect future neighbor relationships.
Approaches
Brute Force Approach
The brute force solution tries every possible order of bursting balloons.
At each step, we choose one balloon to burst, compute the coins gained from its current neighbors, remove it, and recursively solve the smaller problem. Since each recursive level can choose among remaining balloons, the total number of possibilities grows factorially.
This approach is correct because it exhaustively explores all valid bursting sequences and returns the maximum total.
However, it is far too slow. With n = 300, factorial complexity is impossible to compute within any practical time limit.
Key Insight for the Optimal Solution
The major difficulty comes from the fact that bursting a balloon changes neighboring relationships.
Instead of thinking about which balloon to burst first, the critical insight is to think about which balloon is burst last inside a subarray.
Suppose we are solving a subproblem for balloons between indices left and right.
If balloon k is the last balloon burst in that interval:
- Every other balloon inside
(left, right)has already been removed - At that moment, balloon
kis adjacent only toleftandright - Therefore, the coins gained are fixed and easy to compute
This transforms a difficult dynamic problem into an interval dynamic programming problem.
To simplify boundary handling, we add virtual balloons with value 1 at both ends.
If the transformed array is:
[1] + nums + [1]
then the recurrence becomes:
$dp[left][right] = \max_{left < k < right} \left(dp[left][k] + dp[k][right] + nums[left] \times nums[k] \times nums[right]\right)$
This works because:
dp[left][k]represents bursting everything betweenleftandkdp[k][right]represents bursting everything betweenkandrightkis burst last in the interval
The problem becomes a classic interval DP.
Comparison of Approaches
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n!) | O(n) | Tries every possible bursting order recursively |
| Optimal Dynamic Programming | O(n³) | O(n²) | Uses interval DP and chooses the last balloon burst |
Algorithm Walkthrough
- Create a new array with virtual boundary balloons.
We prepend and append 1 to the original array:
balloons = [1] + nums + [1]
This removes special handling for edge balloons because every balloon now always has valid neighbors. 2. Create a 2D DP table.
Let:
dp[left][right]
represent the maximum coins obtainable by bursting all balloons strictly between left and right.
The interval is open, meaning left and right themselves are not burst in this subproblem.
3. Iterate over interval lengths.
We solve smaller intervals before larger intervals because larger intervals depend on smaller ones.
For each interval (left, right):
- Try every possible balloon
kbetween them - Assume
kis the last balloon burst in that interval
- Compute the coins gained if
kis last.
Since all balloons inside the interval except k are already gone:
balloons[left] * balloons[k] * balloons[right]
are the coins earned from bursting k.
5. Add results from subproblems.
The total becomes:
dp[left][k] + dp[k][right] + current_coins
because the left and right intervals are independent once k is fixed as the final balloon.
6. Store the maximum result.
We take the best possible choice of k:
dp[left][right] = max(...)
- Return the answer.
The final answer is:
dp[0][n - 1]
where n is the size of the padded array.
Why it works
The core invariant is that when we choose balloon k as the last balloon burst in interval (left, right), its neighbors are guaranteed to be exactly left and right. That makes the coin gain deterministic.
Because every valid bursting sequence has exactly one last balloon for every interval, the recurrence explores all possibilities without duplication. The DP table ensures each interval is solved once, giving both correctness and efficiency.
Python Solution
from typing import List
class Solution:
def maxCoins(self, nums: List[int]) -> int:
balloons = [1] + nums + [1]
n = len(balloons)
dp = [[0] * n for _ in range(n)]
for length in range(2, n):
for left in range(n - length):
right = left + length
for k in range(left + 1, right):
coins = (
dp[left][k]
+ dp[k][right]
+ balloons[left] * balloons[k] * balloons[right]
)
dp[left][right] = max(dp[left][right], coins)
return dp[0][n - 1]
The implementation begins by adding virtual balloons with value 1 to both ends of the array. This simplifies boundary conditions because every balloon always has neighbors.
The dp table stores solutions for intervals. Initially, all values are 0 because empty intervals produce no coins.
The outer loop iterates through interval sizes from small to large. This ordering is essential because larger intervals depend on smaller subproblems already being solved.
For each interval (left, right), the algorithm tries every possible balloon k as the last balloon burst in that interval.
The recurrence combines:
- The optimal result from the left subinterval
- The optimal result from the right subinterval
- The coins gained from bursting
klast
Finally, dp[0][n - 1] contains the best possible answer for the entire array.
Go Solution
func maxCoins(nums []int) int {
balloons := make([]int, 0, len(nums)+2)
balloons = append(balloons, 1)
for _, num := range nums {
balloons = append(balloons, num)
}
balloons = append(balloons, 1)
n := len(balloons)
dp := make([][]int, n)
for i := range dp {
dp[i] = make([]int, n)
}
for length := 2; length < n; length++ {
for left := 0; left < n-length; left++ {
right := left + length
for k := left + 1; k < right; k++ {
coins := dp[left][k] +
dp[k][right] +
balloons[left]*balloons[k]*balloons[right]
if coins > dp[left][right] {
dp[left][right] = coins
}
}
}
}
return dp[0][n-1]
}
The Go implementation follows the same interval dynamic programming strategy as the Python solution.
Slices are used instead of Python lists, and the 2D DP table is initialized manually using nested make calls.
Go integers are sufficient for this problem because the maximum possible answer fits within standard integer limits under the given constraints.
Worked Examples
Example 1
nums = [3,1,5,8]
After padding:
balloons = [1,3,1,5,8,1]
We build the DP table gradually.
Length = 2
Intervals with exactly one balloon inside.
| Interval | Last Balloon | Coins | DP Value |
|---|---|---|---|
| (0,2) | 1 | 1×3×1 = 3 | 3 |
| (1,3) | 2 | 3×1×5 = 15 | 15 |
| (2,4) | 3 | 1×5×8 = 40 | 40 |
| (3,5) | 4 | 5×8×1 = 40 | 40 |
Length = 3
Consider interval (1,4):
Possible last balloons:
| k | Calculation | Total |
|---|---|---|
| 2 | dp[1][2] + dp[2][4] + 3×1×8 | 0 + 40 + 24 = 64 |
| 3 | dp[1][3] + dp[3][4] + 3×5×8 | 15 + 0 + 120 = 135 |
Best value:
dp[1][4] = 135
Full Result
Eventually:
dp[0][5] = 167
Final answer:
167
Example 2
nums = [1,5]
After padding:
balloons = [1,1,5,1]
Possible orders:
Burst 1 first:
1×1×5 = 5
1×5×1 = 5
Total = 10
Burst 5 first:
1×5×1 = 5
1×1×1 = 1
Total = 6
Optimal answer:
10
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n³) | Three nested loops: interval length, left boundary, and last balloon choice |
| Space | O(n²) | DP table stores results for all intervals |
The DP table contains O(n²) states because every pair (left, right) defines an interval.
For each interval, we iterate through every possible last balloon k, adding another factor of O(n).
Therefore, the total time complexity becomes O(n³).
With n <= 300, this complexity is acceptable.
Test Cases
from typing import List
class Solution:
def maxCoins(self, nums: List[int]) -> int:
balloons = [1] + nums + [1]
n = len(balloons)
dp = [[0] * n for _ in range(n)]
for length in range(2, n):
for left in range(n - length):
right = left + length
for k in range(left + 1, right):
coins = (
dp[left][k]
+ dp[k][right]
+ balloons[left] * balloons[k] * balloons[right]
)
dp[left][right] = max(dp[left][right], coins)
return dp[0][n - 1]
solution = Solution()
assert solution.maxCoins([3, 1, 5, 8]) == 167 # provided example
assert solution.maxCoins([1, 5]) == 10 # provided example
assert solution.maxCoins([1]) == 1 # single balloon
assert solution.maxCoins([0]) == 0 # single zero balloon
assert solution.maxCoins([0, 1, 5]) == 10 # zero inside array
assert solution.maxCoins([1, 1, 1, 1]) == 4 # all identical values
assert solution.maxCoins([9]) == 9 # single large value
assert solution.maxCoins([2, 3]) == 9 # small two-element case
assert solution.maxCoins([8, 2, 6, 8, 1]) == 560 # larger mixed case
assert solution.maxCoins([7, 9, 8, 0, 7, 1, 3, 5]) == 1358 # stress-style case
Test Summary
| Test | Why |
|---|---|
[3,1,5,8] |
Validates standard example |
[1,5] |
Smallest non-trivial case |
[1] |
Single balloon handling |
[0] |
Zero-value balloon |
[0,1,5] |
Ensures zeros are handled correctly |
[1,1,1,1] |
Repeated identical values |
[9] |
Single large balloon |
[2,3] |
Small interval correctness |
[8,2,6,8,1] |
Complex mixed interactions |
[7,9,8,0,7,1,3,5] |
Larger stress scenario |
Edge Cases
Single Balloon
When the input contains only one balloon, the algorithm must correctly treat its neighbors as virtual balloons with value 1.
For example:
nums = [5]
The result should be:
1 × 5 × 1 = 5
Without boundary padding, implementations often produce index errors or incorrect neighbor calculations. The padded array technique handles this naturally.
Balloons Containing Zero
Zero-valued balloons can dramatically change the optimal bursting order.
For example:
nums = [0, 1, 5]
Bursting the zero too late may eliminate opportunities for larger products. A greedy approach can easily fail here.
The interval DP solution explores every possible last balloon for every interval, guaranteeing that zero values are handled correctly.
Very Small Intervals
Intervals with only one balloon inside are the base building blocks of the DP table.
For interval (left, right) where:
right = left + 2
there is exactly one possible balloon to burst last.
Incorrect interval iteration order can leave required subproblems uncomputed. By processing intervals in increasing length order, the implementation guarantees all dependencies are already solved before they are needed.