120.Burst Balloons
Problem
You have n balloons in a row, balloon i painted with the number nums[i]. When you burst balloon i you earn nums[left] * nums[i] * nums[right] coins, where left and right are the balloons currently adjacent to i (after earlier bursts). If a neighbour does not exist, treat it as a balloon with value 1.
Burst all the balloons and return the maximum coins you can collect.
Examples
Input: nums = [3, 1, 5, 8]
Output: 167
Explanation: [3,1,5,8] → burst 1: 3*1*5 = 15
[3,5,8] → burst 5: 3*5*8 = 120
[3,8] → burst 3: 1*3*8 = 24
[8] → burst 8: 1*8*1 = 8
total = 15 + 120 + 24 + 8 = 167Input: nums = [1, 5]
Output: 10
Explanation: burst 1 first (1*1*5 = 5), then 5 (1*5*1 = 5) = 10.Constraints
n == nums.length1 <= n <= 3000 <= nums[i] <= 100
💡 Key Insight
Choosing which balloon to burst first is hopeless — afterwards its two neighbours become adjacent and the subproblems are entangled. Instead choose which balloon k to burst last in an open interval (l, r): while every other balloon between l and r is being burst, k is still standing, so the left part (l, k) and right part (k, r) are completely independent. When k finally bursts, its neighbours are exactly the boundaries l and r. Pad the array with 1 on both ends and:
dp[l][r] = max over l < k < r of ( dp[l][k] + vals[l] * vals[k] * vals[r] + dp[k][r] )
Brute Force (Try Every Burst Order)
TimeO(n · n!)SpaceO(n)Intuition
Simulate literally: pick any remaining balloon to burst next, collect its coins from its current neighbours, recurse on the remaining list, and undo. This explores all n! orders.
Algorithm
best(list): if the list is empty return0.- For each index
iin the list: coins =left * list[i] * right(using1beyond the ends); removelist[i], recurse, add it back; keep the max ofcoins + recursive result. - Return
best(nums as list).
Visualization
flowchart TD
A["[1,5]"] -->|"burst 1: 1*1*5 = 5"| B["[5]"]
A -->|"burst 5: 1*5*1 = 5"| C["[1]"]
B -->|"burst 5: 1*5*1 = 5"| D["total 10"]
C -->|"burst 1: 1*1*1 = 1"| E["total 6"]
D --> F["max = 10"]
E --> FCode
import java.util.ArrayList;
import java.util.List;
class Solution {
public int maxCoins(int[] nums) {
List<Integer> list = new ArrayList<>();
for (int x : nums) {
list.add(x);
}
return best(list);
}
private int best(List<Integer> list) {
if (list.isEmpty()) {
return 0;
}
int result = 0;
for (int i = 0; i < list.size(); i++) {
int left = i > 0 ? list.get(i - 1) : 1;
int right = i < list.size() - 1 ? list.get(i + 1) : 1;
int coins = left * list.get(i) * right;
int removed = list.remove(i); // burst balloon i
result = Math.max(result, coins + best(list));
list.add(i, removed); // undo
}
return result;
}
}Complexity
- Time: O(n · n!) — every permutation of burst order, with O(n) list work per step.
- Space: O(n) — recursion depth and the list.
Top-Down Memoization (Interval DP)
TimeO(n³)SpaceO(n²)Intuition
Use the "last balloon" idea. Pad: vals = [1, nums..., 1]. solve(l, r) = max coins from bursting every balloon strictly between l and r, given that l and r are still standing. Try each k in (l, r) as the last one.
Algorithm
- Build
valsof lengthn + 2;memo[n+2][n+2],-1= unknown. solve(l, r):- If
l + 1 == r, return0(nothing between). - Return
max over k in (l, r)ofsolve(l, k) + vals[l]*vals[k]*vals[r] + solve(k, r).
- If
- Return
solve(0, n + 1).
Dry Run
flowchart TD
A["solve(0,5)<br/>vals [1,3,1,5,8,1]"] -->|"k=4 (8) last: 1*8*1"| B["solve(0,4) + 8 + solve(4,5)"]
B --> C["solve(0,4)<br/>k=1 (3) last: 1*3*8 = 24"]
C --> D["solve(0,1) = 0"]
C --> E["solve(1,4)<br/>k=3 (5) last: 3*5*8 = 120"]
E --> F["solve(1,3)<br/>k=2 (1): 3*1*5 = 15"]
E --> G["solve(3,4) = 0"]
F --> H["solve(1,4) = 15 + 120 = 135"]
H --> I["solve(0,4) = 24 + 135 = 159"]
I --> J["solve(0,5) = 159 + 8 = 167"]Code
import java.util.Arrays;
class Solution {
private int[][] memo;
private int[] vals;
public int maxCoins(int[] nums) {
int n = nums.length;
vals = new int[n + 2];
vals[0] = vals[n + 1] = 1; // virtual boundary balloons
for (int i = 0; i < n; i++) {
vals[i + 1] = nums[i];
}
memo = new int[n + 2][n + 2];
for (int[] row : memo) {
Arrays.fill(row, -1);
}
return solve(0, n + 1);
}
// max coins from bursting everything strictly between l and r
private int solve(int l, int r) {
if (l + 1 == r) {
return 0;
}
if (memo[l][r] != -1) {
return memo[l][r];
}
int best = 0;
for (int k = l + 1; k < r; k++) {
// k is burst last, so its neighbours at that moment are l and r
int coins = solve(l, k) + vals[l] * vals[k] * vals[r] + solve(k, r);
best = Math.max(best, coins);
}
memo[l][r] = best;
return best;
}
}Complexity
- Time: O(n³) — O(n²) intervals, O(n) choices of
keach. - Space: O(n²) — the memo table, plus O(n) recursion depth.
Bottom-Up Interval DPOptimal
TimeO(n³)SpaceO(n²)Intuition
Fill dp[l][r] in order of increasing interval length, so the shorter intervals dp[l][k] and dp[k][r] are ready before they are needed. There is no 1-D compression here — an interval depends on many cells in its row and column — so O(n²) space is the floor for this DP.
Algorithm
- Build padded
vals(lengthN = n + 2);dp = new int[N][N](zeros). - For
len = 2..N-1(distance betweenlandr), for eachlwithr = l + len < N:dp[l][r] = max over k in (l, r) of dp[l][k] + vals[l]*vals[k]*vals[r] + dp[k][r]. - Return
dp[0][N-1].
Dry Run
vals = [1, 3, 1, 5, 8, 1] (indices 0..5), showing dp[l][r] for r > l + 1:
| l \ r | 2 | 3 | 4 | 5 |
|---|---|---|---|---|
| 0 | 3 | 30 | 159 | 167 |
| 1 | 15 | 135 | 159 | |
| 2 | 40 | 48 | ||
| 3 | 40 |
For example dp[1][4]: k=2 → 0 + 3·1·8 + 40 = 64, k=3 → 15 + 3·5·8 + 0 = 135, so 135.
And dp[0][5]: the best k is 4 (balloon 8 last): 159 + 1·8·1 + 0 = 167.
Code
class Solution {
public int maxCoins(int[] nums) {
int n = nums.length;
int size = n + 2;
int[] vals = new int[size];
vals[0] = vals[size - 1] = 1; // padding balloons
for (int i = 0; i < n; i++) {
vals[i + 1] = nums[i];
}
int[][] dp = new int[size][size]; // dp[l][r]: open interval (l, r)
for (int len = 2; len < size; len++) {
for (int l = 0; l + len < size; l++) {
int r = l + len;
for (int k = l + 1; k < r; k++) {
// burst k last inside (l, r)
int coins = dp[l][k] + vals[l] * vals[k] * vals[r] + dp[k][r];
if (coins > dp[l][r]) {
dp[l][r] = coins;
}
}
}
}
return dp[0][size - 1];
}
}Complexity
- Time: O(n³) — about
n³ / 6inner iterations, ~4.5 million forn = 300. - Space: O(n²) — the interval table.
Complexity Comparison
| Approach | Time | Space |
|---|---|---|
| Brute Force (Try Every Burst Order) | O(n · n!) | O(n) |
| Top-Down Memoization (Interval DP) | O(n³) | O(n²) |
| Bottom-Up Interval DP | O(n³) | O(n²) |
Common Pitfalls
- Choosing the balloon burst first in an interval — its neighbours then depend on outside choices, so subproblems are not independent. Always choose the last one.
- Using closed intervals without padding — the boundary multipliers
vals[l]andvals[r]must be balloons that are still present (or the virtual1s). - Iterating
lfrom 0 upward withrgrowing in the outer loop incorrectly — fill by increasing length (orldescending,rascending) so dependencies are ready. - Treating
0-valued balloons specially — they just contribute0coins; the DP handles them. - Forgetting that the answer is
dp[0][n+1]on the padded array, notdp[0][n-1].