Burst Balloons
You are given an array nums where nums[i] is the number painted on the i-th balloon. You burst all the balloons one at a time, in any order you choose. Bursting balloon i earns nums[left] × nums[i] × nums[right] coins, where left and right are the balloons immediately adjacent to i among those still unburst; if a side has no remaining balloon, treat it as a 1. After a balloon bursts, its neighbours become adjacent to each other. Return the maximum coins you can collect.
- 1 ≤ nums.length ≤ 300
- 0 ≤ nums[i] ≤ 100
- Out-of-range neighbours count as 1, not 0
- Every balloon must eventually be burst
nums = [3, 1, 5, 8]167nums = [1, 5]10nums = [7]7nums = [2, 3]9nums = [0, 5, 0]5Bursting a balloon pays you the product of it and its two current neighbours, and then the array closes up. So the array is constantly changing shape, and which balloons are adjacent depends on everything you have already burst.
The natural first thought is to pick a balloon to burst first and recurse on what remains. Try to write down that state and you immediately hit a wall. After the first burst, the remaining balloons are the original array minus one element — that is not a contiguous stretch of the original, it is an arbitrary subset. To describe it you would need a set of surviving indices, and there are 2ⁿ of those. The table would have more entries than atoms in the room.
Worse, the subproblems are not independent. Bursting a balloon in the left half changes who the neighbours are in the middle, so the two halves cannot be solved separately.
Consider a stretch of balloons from index i to index j, and suppose we are going to burst all of them while everything outside that stretch is still intact. One of them is burst last; call it k.
Now the payoff. At the moment k bursts, every other balloon in i..j is already gone, so its two neighbours are whatever sits immediately outside the range: nums[i-1] and nums[j+1]. Those are known, fixed values that do not depend on the order used inside the range. So bursting k last earns exactly
nums[i-1] × nums[k] × nums[j+1]
And before that, the balloons in i..k-1 were all burst — with k still standing as their right-hand wall — and the balloons in k+1..j were all burst, with k still standing as their left-hand wall. Those two groups never interact, because k sits between them the entire time and never disappears until the end. Each is an independent subproblem of exactly the same kind.
That is the whole solution, and it is worth checking why the "first" framing failed by comparison: if you burst k first, the two sides become adjacent immediately and their fates are entangled. Bursting it last keeps them separated by a wall. The last-burst view is what buys independence.
f(i, j) is the maximum coins obtainable by bursting every balloon strictly inside the open range (i, j), assuming balloons i and j are still standing.Using open boundaries makes the arithmetic cleaner than inclusive ones. With that definition:
f(i, j) = max over k strictly between i and j of [ f(i, k) + f(k, j) + nums[i] × nums[k] × nums[j] ]
Base case: if j == i + 1 there is nothing between them, so f = 0.
The statement says a missing neighbour counts as 1. Rather than writing boundary checks everywhere, add a 1 at each end of the array. Multiplying by 1 changes nothing, so the padded array behaves identically while every real balloon now has genuine neighbours on both sides.
def max_coins(nums):
balloons = [1] + nums + [1]
n = len(balloons)
memo = {}
def f(i, j):
if j <= i + 1: return 0 # nothing strictly between
if (i, j) in memo: return memo[(i, j)]
best = 0
for k in range(i + 1, j): # k is burst LAST in this range
coins = (f(i, k) + f(k, j)
+ balloons[i] * balloons[k] * balloons[j])
best = max(best, coins)
memo[(i, j)] = best
return best
return f(0, n - 1)The answer is f(0, n-1) — burst everything strictly between the two padded ones, which is exactly the original array.
Cost: about N² intervals, each trying up to N split points, giving O(N³) time and O(N²) space. For 300 balloons that is around 27 million operations, which runs comfortably.
As in matrix chain, the sub-intervals a cell needs are always shorter than the cell itself, but their endpoints do not move monotonically. So iterate by length, shortest first:
def max_coins(nums):
balloons = [1] + nums + [1]
n = len(balloons)
dp = [[0] * n for _ in range(n)]
for length in range(2, n): # distance between the open boundaries
for i in range(0, n - length):
j = i + length
for k in range(i + 1, j):
dp[i][j] = max(dp[i][j],
dp[i][k] + dp[k][j]
+ balloons[i] * balloons[k] * balloons[j])
return dp[0][n - 1]k is the balloon burst last, not first, and the two recursive calls f(i, k) and f(k, j) both keep k as an endpoint — it is a standing wall in both, not a member of either. If you find yourself writing f(i, k-1) and f(k+1, j), you have slipped back into the inclusive-range habit from other interval problems and the neighbour arithmetic will come out wrong.[1, 3, 1, 5, 8, 1], indices 0 to 5.Working up by interval length, the small ranges are straightforward — for example f(0, 2) has only k = 1 available, earning 1 × 3 × 1 = 3. Skipping to the finished answer, the optimal order the table discovers is:
[3, 5, 8].[3, 8].[8].Total 15 + 120 + 24 + 8 = 167.
Read that in reverse and you can see the recurrence's view: the balloon burst last over the whole range was the 8, and everything to its left was cleared first while the 8 stood as a wall.
A small case worth checking by hand: nums = [1, 5], padded to [1, 1, 5, 1]. Burst the 1 first for 1 × 1 × 5 = 5, then the 5 for 1 × 5 × 1 = 5, totalling 10. The other order gives 1 × 5 × 1 = 5 then 1 × 1 × 1 = 1, totalling 6. So the answer is 10, and the greedy instinct to burst the biggest balloon first is wrong — big balloons are worth more as neighbours than as targets, so you generally want them alive as long as possible.
Every other problem in this section had an obvious direction of progress: the next step, the next index, the next day. This one does not, and the skill being tested is what to do when that is the case.
The recipe: if fixing the first move leaves a mess, fix the last one. The test for whether you have chosen well is always the same — does the remaining problem break into independent pieces of the same kind? Here, choosing the last balloon in a range left two sides permanently separated by a standing wall. Choosing the first left two sides that immediately merged.
The second lesson is about the padding. Adding sentinel 1s at the ends turned a rule with special cases into plain arithmetic. Look for that move whenever a problem's edges behave differently from its middle: an identity element at the boundary (1 for products, 0 for sums, infinity for minima) usually erases the special case entirely, and erased special cases cannot contain bugs.
Finally, note how the greedy instinct inverted here. Normally you grab the big prize first; here the big values are most valuable while they are still standing, because they multiply their neighbours' payoffs. Whenever an element's value depends on its context rather than on itself, expect greedy reasoning to fail and expect the order of operations to be the real problem.