|
| 1 | +# 2.20 Burst Balloons |
| 2 | + |
| 3 | +> **Source:** [`src/main/kotlin/array/dp/BurstBallonsClean.kt`](https://github.com/arpanpathak/AdvancedAlgorithmPatterns/blob/main/src/main/kotlin/array/dp/BurstBallonsClean.kt) (+ `BurstBaloons.kt`) |
| 4 | +> **Pattern:** interval DP with sentinels · **Core page** |
| 5 | +
|
| 6 | +## The Problem |
| 7 | + |
| 8 | +Given `nums[i]` (balloon values), burst them one by one; bursting balloon `i` scores `nums[left] * nums[i] * nums[right]` (neighbors at that moment). Maximize total score. |
| 9 | + |
| 10 | +- Constraints: $1 \le n \le 300$; values fit in `Int`. |
| 11 | + |
| 12 | +## Examples |
| 13 | + |
| 14 | +``` |
| 15 | +Input: nums = [3,1,5,8] -> Output: 167 (burst order 1, 5, 3, 8) |
| 16 | +Input: nums = [1,5] -> Output: 10 (5 + 1·5·1... the padded view: burst 5 then 1) |
| 17 | +``` |
| 18 | + |
| 19 | +## Intuition — think in terms of the *last* balloon, padded with sentinel 1s |
| 20 | + |
| 21 | +The classic trick: **work backwards**. Define `solve(left, right)` = max coins from bursting all balloons *between* `left` and `right` (exclusive), assuming `left` and `right` are **already-burst sentinels** that stay. Then for the *last* balloon `k` burst in that range, its neighbors are exactly `balloons[left]` and `balloons[right]` — the pad `[1] + nums + [1]` makes the boundary scoring uniform: |
| 22 | + |
| 23 | +$$ |
| 24 | +solve(left, right) = \max_{k \in (left, right)} \Big( A_{left} \cdot A_k \cdot A_{right} + solve(left, k) + solve(k, right) \Big) |
| 25 | +$$ |
| 26 | + |
| 27 | +**Why "last balloon" and not "first"?** If `k` is burst *last*, the sub-ranges `(left, k)` and `(k, right)` are independent — their balloons are gone before `k` pops, and `k`'s neighbors are the fixed sentinels. Burst-first thinking entangles the ranges; burst-last decomposes them. This is the [2.10](minimum-cost-to-cut-a-stick.md) interval-DP structure with the direction flipped. |
| 28 | + |
| 29 | +**The repo's one-expression form** (`BurstBallonsClean.kt`): |
| 30 | + |
| 31 | +```kotlin |
| 32 | +typealias State = Pair<Int, Int> |
| 33 | + |
| 34 | +fun maxCoins(nums: IntArray): Int { |
| 35 | + val ballons = intArrayOf(1) + nums + 1 // sentinels |
| 36 | + val cache = mutableMapOf<State, Int>() |
| 37 | + |
| 38 | + fun solve(left: Int, right: Int): Int = cache.getOrPut(left to right) { |
| 39 | + when { |
| 40 | + left > right -> 0 // empty range |
| 41 | + else -> (left..right).maxOf { k -> |
| 42 | + ballons[left - 1] * ballons[k] * ballons[right + 1] + |
| 43 | + solve(left, k - 1) + solve(k + 1, right) |
| 44 | + } |
| 45 | + } |
| 46 | + } |
| 47 | + return solve(1, ballons.size - 2) |
| 48 | +} |
| 49 | +``` |
| 50 | + |
| 51 | +**What's cool:** the whole recurrence is a `getOrPut` + `maxOf` (the [19.12-era](../ch02-dynamic-programming/pattern-primer.md) one-expression DP style, now in the main chapter). `solve(1, size - 2)` is "all real balloons, sentinels outside". |
| 52 | + |
| 53 | +## Approach 1 — Brute force burst orders (n!) |
| 54 | + |
| 55 | +Try every permutation: correct, factorial — the baseline. |
| 56 | + |
| 57 | +## Approach 2 — Interval DP with sentinels (the repo's clean version, optimal) |
| 58 | + |
| 59 | +```kotlin |
| 60 | +class BurstBallonsClean { |
| 61 | + fun maxCoins(nums: IntArray): Int { |
| 62 | + val ballons = intArrayOf(1) + nums + 1 |
| 63 | + val cache = mutableMapOf<Pair<Int, Int>, Int>() |
| 64 | + |
| 65 | + fun solve(left: Int, right: Int): Int = cache.getOrPut(left to right) { |
| 66 | + when { |
| 67 | + left > right -> 0 |
| 68 | + else -> (left..right).maxOf { k -> |
| 69 | + ballons[left - 1] * ballons[k] * ballons[right + 1] + |
| 70 | + solve(left, k - 1) + solve(k + 1, right) |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | + return solve(1, ballons.size - 2) |
| 75 | + } |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +```java |
| 80 | +import java.util.*; |
| 81 | + |
| 82 | +public class BurstBalloons { |
| 83 | + /** |
| 84 | + * @param nums balloon values |
| 85 | + * @return max coins from bursting all balloons |
| 86 | + */ |
| 87 | + public int maxCoins(int[] nums) { |
| 88 | + int n = nums.length; |
| 89 | + int[] a = new int[n + 2]; |
| 90 | + a[0] = a[n + 1] = 1; // sentinels |
| 91 | + for (int i = 0; i < n; i++) a[i + 1] = nums[i]; |
| 92 | + |
| 93 | + int[][] dp = new int[n + 2][n + 2]; |
| 94 | + |
| 95 | + for (int len = 1; len <= n; len++) { // window length |
| 96 | + for (int left = 1; left + len - 1 <= n; left++) { |
| 97 | + int right = left + len - 1; |
| 98 | + for (int k = left; k <= right; k++) { |
| 99 | + int score = a[left - 1] * a[k] * a[right + 1] |
| 100 | + + dp[left][k - 1] + dp[k + 1][right]; |
| 101 | + dp[left][right] = Math.max(dp[left][right], score); |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + return dp[1][n]; |
| 106 | + } |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +```cpp |
| 111 | +#include <vector> |
| 112 | +#include <algorithm> |
| 113 | + |
| 114 | +class BurstBalloons { |
| 115 | +public: |
| 116 | + /** |
| 117 | + * @param nums balloon values |
| 118 | + * @return max coins from bursting all balloons |
| 119 | + */ |
| 120 | + int maxCoins(std::vector<int>& nums) { |
| 121 | + int n = nums.size(); |
| 122 | + std::vector<int> a(n + 2, 1); // sentinels |
| 123 | + for (int i = 0; i < n; i++) a[i + 1] = nums[i]; |
| 124 | + |
| 125 | + std::vector<std::vector<int>> dp(n + 2, std::vector<int>(n + 2, 0)); |
| 126 | + |
| 127 | + for (int len = 1; len <= n; len++) { |
| 128 | + for (int left = 1; left + len - 1 <= n; left++) { |
| 129 | + int right = left + len - 1; |
| 130 | + for (int k = left; k <= right; k++) { |
| 131 | + dp[left][right] = std::max(dp[left][right], |
| 132 | + a[left - 1] * a[k] * a[right + 1] + dp[left][k - 1] + dp[k + 1][right]); |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + return dp[1][n]; |
| 137 | + } |
| 138 | +}; |
| 139 | +``` |
| 140 | + |
| 141 | +```python |
| 142 | +def max_coins(nums: list[int]) -> int: |
| 143 | + """ |
| 144 | + @param nums: balloon values |
| 145 | + @return: max coins from bursting all balloons |
| 146 | + """ |
| 147 | + a = [1] + nums + [1] # sentinels |
| 148 | + n = len(nums) |
| 149 | + dp = [[0] * (n + 2) for _ in range(n + 2)] |
| 150 | + |
| 151 | + for length in range(1, n + 1): # window length |
| 152 | + for left in range(1, n - length + 2): |
| 153 | + right = left + length - 1 |
| 154 | + for k in range(left, right + 1): |
| 155 | + score = a[left - 1] * a[k] * a[right + 1] + dp[left][k - 1] + dp[k + 1][right] |
| 156 | + dp[left][right] = max(dp[left][right], score) |
| 157 | + return dp[1][n] |
| 158 | +``` |
| 159 | + |
| 160 | +```rust |
| 161 | +impl Solution { |
| 162 | + /// @param nums balloon values |
| 163 | + /// @return max coins from bursting all balloons |
| 164 | + pub fn max_coins(nums: Vec<i32>) -> i32 { |
| 165 | + let n = nums.len(); |
| 166 | + let mut a = vec![1; n + 2]; // sentinels |
| 167 | + for (i, &v) in nums.iter().enumerate() { a[i + 1] = v; } |
| 168 | + |
| 169 | + let mut dp = vec![vec![0i32; n + 2]; n + 2]; |
| 170 | + |
| 171 | + for len in 1..=n { |
| 172 | + for left in 1..=(n - len + 1) { |
| 173 | + let right = left + len - 1; |
| 174 | + for k in left..=right { |
| 175 | + dp[left][right] = dp[left][right].max( |
| 176 | + a[left - 1] * a[k] * a[right + 1] + dp[left][k - 1] + dp[k + 1][right]); |
| 177 | + } |
| 178 | + } |
| 179 | + } |
| 180 | + dp[1][n] |
| 181 | + } |
| 182 | +} |
| 183 | +``` |
| 184 | + |
| 185 | +## Dry run |
| 186 | + |
| 187 | +**Input:** `nums = [3,1,5,8]`. Padded: `a = [1,3,1,5,8,1]`. |
| 188 | + |
| 189 | +``` |
| 190 | +length-1 windows: dp[1][1] = a[0]*a[1]*a[2] = 1*3*1 = 3. (burst 3 alone: 3) |
| 191 | + dp[2][2] = a[1]*a[2]*a[3] = 3*1*5 = 15. dp[3][3] = 1*5*8 = 40. dp[4][4] = 5*8*1 = 40. |
| 192 | +length-2: dp[1][2] = max(k=1: a0*a1*a3 + dp[2][2] = 1*3*5+15 = 30, |
| 193 | + k=2: a0*a2*a3 + dp[1][1] = 1*1*5+3 = 8) = 30. |
| 194 | + dp[2][3] = max(k=2: a1*a2*a4 + dp[3][3] = 3*1*8+40 = 64, |
| 195 | + k=3: a1*a3*a4 + dp[2][2] = 3*5*8+15 = 135) = 135. |
| 196 | + dp[3][4] = max(k=3: a2*a3*a5 + dp[4][4] = 1*5*1+40 = 45, |
| 197 | + k=4: a2*a4*a5 + dp[3][3] = 1*8*1+40 = 48) = 48. |
| 198 | +length-3: dp[1][3] = max(k=1: a0*a1*a4 + dp[2][3] = 1*3*8+135 = 159, |
| 199 | + k=2: a0*a2*a4 + dp[1][1]+dp[3][3] = 1*1*8+3+40 = 51, |
| 200 | + k=3: a0*a3*a4 + dp[1][2] = 1*5*8+30 = 70) = 159. |
| 201 | +length-4: dp[1][4] = max(k=1: a0*a1*a5 + dp[2][4] = 1*3*1+48 = 51, |
| 202 | + k=2: a0*a2*a5 + dp[1][1]+dp[3][4] = 1*1*1+3+48 = 52, |
| 203 | + k=3: a0*a3*a5 + dp[1][2]+dp[4][4] = 1*5*1+30+40 = 75, |
| 204 | + k=4: a0*a4*a5 + dp[1][3] = 1*8*1+159 = 167) = 167. |
| 205 | +
|
| 206 | +Output: 167 ✓ |
| 207 | +``` |
| 208 | + |
| 209 | +The last-balloon reading of the winning `k=4` (burst 8 last): the 8's neighbors are the sentinels `a[0]` and `a[5]` (both 1) → 8 points, plus the optimal `dp[1][3] = 159` from the remaining three — the ranges decompose exactly because 8 goes last. |
| 210 | + |
| 211 | +## Complexity |
| 212 | + |
| 213 | +**Time.** All (left, k, right) triples: |
| 214 | + |
| 215 | +$$ |
| 216 | +T(n) = O(n^3) |
| 217 | +$$ |
| 218 | + |
| 219 | +**Space.** The interval table: |
| 220 | + |
| 221 | +$$ |
| 222 | +S(n) = O(n^2) |
| 223 | +$$ |
| 224 | + |
| 225 | +## Variants & follow-ups |
| 226 | + |
| 227 | +- **Minimum Cost To Cut A Stick** ([2.10](minimum-cost-to-cut-a-stick.md)) — the interval-DP sibling (min instead of max, cut-first instead of burst-last). |
| 228 | +- **Stone Game** — the zero-sum interval DP (now a section on the [2.10](minimum-cost-to-cut-a-stick.md) page). |
| 229 | +- **Interview follow-up:** "Why pad with 1s?" Without sentinels, the *first* and *last* bursts have only one neighbor — a special case. The `[1] + nums + [1]` pad makes every burst uniformly `left × k × right`, so the recurrence needs no boundary branches. |
0 commit comments