1073-burst-balloons — Burst Balloons
Tutorial
The Problem
The burst balloons problem is an interval DP gem: given a row of balloons with numbers, bursting balloon k earns nums[k-1] * nums[k] * nums[k+1] coins. Find the maximum coins from bursting all balloons in the best order. The key insight is reasoning about the LAST balloon to burst in an interval, not the first.
This "think backwards" trick converts an exponential search into a polynomial DP, and the same pattern appears in matrix chain multiplication, optimal binary search trees, and stone merging problems.
🎯 Learning Outcomes
dp[i][j] = max coins for interval (i, j) exclusiveCode Example
#![allow(clippy::all)]
// 1073: Burst Balloons — Interval DP
use std::collections::HashMap;
// Approach 1: Bottom-up interval DP
fn max_coins_dp(nums: &[i32]) -> i32 {
let n = nums.len();
let mut balloons = vec![1i32; n + 2];
for i in 0..n {
balloons[i + 1] = nums[i];
}
let len = n + 2;
let mut dp = vec![vec![0i32; len]; len];
for gap in 2..len {
for i in 0..len - gap {
let j = i + gap;
for k in (i + 1)..j {
let coins = dp[i][k] + dp[k][j] + balloons[i] * balloons[k] * balloons[j];
dp[i][j] = dp[i][j].max(coins);
}
}
}
dp[0][len - 1]
}
// Approach 2: Recursive with memoization
fn max_coins_memo(nums: &[i32]) -> i32 {
let n = nums.len();
let mut balloons = vec![1i32; n + 2];
for i in 0..n {
balloons[i + 1] = nums[i];
}
let len = n + 2;
fn solve(
left: usize,
right: usize,
balloons: &[i32],
cache: &mut HashMap<(usize, usize), i32>,
) -> i32 {
if right.saturating_sub(left) < 2 {
return 0;
}
if let Some(&v) = cache.get(&(left, right)) {
return v;
}
let mut best = 0;
for k in (left + 1)..right {
let coins = solve(left, k, balloons, cache)
+ solve(k, right, balloons, cache)
+ balloons[left] * balloons[k] * balloons[right];
best = best.max(coins);
}
cache.insert((left, right), best);
best
}
let mut cache = HashMap::new();
solve(0, len - 1, &balloons, &mut cache)
}
// Approach 3: Divide and conquer (same logic, different framing)
fn max_coins_dc(nums: &[i32]) -> i32 {
let n = nums.len();
let mut balloons = vec![1i32; n + 2];
for i in 0..n {
balloons[i + 1] = nums[i];
}
let len = n + 2;
let mut memo = vec![vec![-1i32; len]; len];
fn solve(l: usize, r: usize, balloons: &[i32], memo: &mut Vec<Vec<i32>>) -> i32 {
if r.saturating_sub(l) < 2 {
return 0;
}
if memo[l][r] >= 0 {
return memo[l][r];
}
let mut best = 0;
for k in (l + 1)..r {
let coins = solve(l, k, balloons, memo)
+ solve(k, r, balloons, memo)
+ balloons[l] * balloons[k] * balloons[r];
best = best.max(coins);
}
memo[l][r] = best;
best
}
solve(0, len - 1, &balloons, &mut memo)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dp() {
assert_eq!(max_coins_dp(&[3, 1, 5, 8]), 167);
assert_eq!(max_coins_dp(&[1, 5]), 10);
assert_eq!(max_coins_dp(&[1]), 1);
}
#[test]
fn test_memo() {
assert_eq!(max_coins_memo(&[3, 1, 5, 8]), 167);
assert_eq!(max_coins_memo(&[1, 5]), 10);
}
#[test]
fn test_dc() {
assert_eq!(max_coins_dc(&[3, 1, 5, 8]), 167);
assert_eq!(max_coins_dc(&[1, 5]), 10);
}
}Key Differences
vec![1; n+2] with array blit, OCaml uses Array.make.usize::MAX risk**: Rust must avoid usize::MAX initialization for max-problems; using 0 and taking max works here since coins are non-negative.OCaml Approach
let max_coins nums =
let n = Array.length nums in
let b = Array.make (n+2) 1 in
Array.blit nums 0 b 1 n;
let len = n + 2 in
let dp = Array.make_matrix len len 0 in
for gap = 2 to len - 1 do
for i = 0 to len - gap - 1 do
let j = i + gap in
for k = i + 1 to j - 1 do
let coins = dp.(i).(k) + dp.(k).(j) + b.(i) * b.(k) * b.(j) in
if coins > dp.(i).(j) then dp.(i).(j) <- coins
done
done
done;
dp.(0).(len-1)
Structurally identical. The sentinel-insertion and interval-filling logic is purely mathematical.
Full Source
#![allow(clippy::all)]
// 1073: Burst Balloons — Interval DP
use std::collections::HashMap;
// Approach 1: Bottom-up interval DP
fn max_coins_dp(nums: &[i32]) -> i32 {
let n = nums.len();
let mut balloons = vec![1i32; n + 2];
for i in 0..n {
balloons[i + 1] = nums[i];
}
let len = n + 2;
let mut dp = vec![vec![0i32; len]; len];
for gap in 2..len {
for i in 0..len - gap {
let j = i + gap;
for k in (i + 1)..j {
let coins = dp[i][k] + dp[k][j] + balloons[i] * balloons[k] * balloons[j];
dp[i][j] = dp[i][j].max(coins);
}
}
}
dp[0][len - 1]
}
// Approach 2: Recursive with memoization
fn max_coins_memo(nums: &[i32]) -> i32 {
let n = nums.len();
let mut balloons = vec![1i32; n + 2];
for i in 0..n {
balloons[i + 1] = nums[i];
}
let len = n + 2;
fn solve(
left: usize,
right: usize,
balloons: &[i32],
cache: &mut HashMap<(usize, usize), i32>,
) -> i32 {
if right.saturating_sub(left) < 2 {
return 0;
}
if let Some(&v) = cache.get(&(left, right)) {
return v;
}
let mut best = 0;
for k in (left + 1)..right {
let coins = solve(left, k, balloons, cache)
+ solve(k, right, balloons, cache)
+ balloons[left] * balloons[k] * balloons[right];
best = best.max(coins);
}
cache.insert((left, right), best);
best
}
let mut cache = HashMap::new();
solve(0, len - 1, &balloons, &mut cache)
}
// Approach 3: Divide and conquer (same logic, different framing)
fn max_coins_dc(nums: &[i32]) -> i32 {
let n = nums.len();
let mut balloons = vec![1i32; n + 2];
for i in 0..n {
balloons[i + 1] = nums[i];
}
let len = n + 2;
let mut memo = vec![vec![-1i32; len]; len];
fn solve(l: usize, r: usize, balloons: &[i32], memo: &mut Vec<Vec<i32>>) -> i32 {
if r.saturating_sub(l) < 2 {
return 0;
}
if memo[l][r] >= 0 {
return memo[l][r];
}
let mut best = 0;
for k in (l + 1)..r {
let coins = solve(l, k, balloons, memo)
+ solve(k, r, balloons, memo)
+ balloons[l] * balloons[k] * balloons[r];
best = best.max(coins);
}
memo[l][r] = best;
best
}
solve(0, len - 1, &balloons, &mut memo)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dp() {
assert_eq!(max_coins_dp(&[3, 1, 5, 8]), 167);
assert_eq!(max_coins_dp(&[1, 5]), 10);
assert_eq!(max_coins_dp(&[1]), 1);
}
#[test]
fn test_memo() {
assert_eq!(max_coins_memo(&[3, 1, 5, 8]), 167);
assert_eq!(max_coins_memo(&[1, 5]), 10);
}
#[test]
fn test_dc() {
assert_eq!(max_coins_dc(&[3, 1, 5, 8]), 167);
assert_eq!(max_coins_dc(&[1, 5]), 10);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dp() {
assert_eq!(max_coins_dp(&[3, 1, 5, 8]), 167);
assert_eq!(max_coins_dp(&[1, 5]), 10);
assert_eq!(max_coins_dp(&[1]), 1);
}
#[test]
fn test_memo() {
assert_eq!(max_coins_memo(&[3, 1, 5, 8]), 167);
assert_eq!(max_coins_memo(&[1, 5]), 10);
}
#[test]
fn test_dc() {
assert_eq!(max_coins_dc(&[3, 1, 5, 8]), 167);
assert_eq!(max_coins_dc(&[1, 5]), 10);
}
}
Deep Comparison
Burst Balloons — Comparison
Core Insight
Burst balloons is interval DP: think about which balloon to burst last in each subinterval. Adding virtual boundary balloons (value 1) simplifies edge cases. dp[i][j] = max coins obtainable from all balloons strictly between i and j.
OCaml Approach
Array.init (n+2) for padded balloon arraymax for tracking best splitHashtbl with tuple keys for memoizationRust Approach
vec![1i32; n+2] for padded array.max() chained methodHashMap and 2D Vec<Vec<i32>> with -1 sentinelComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Padded array | Array.init (n+2) (fun i -> ...) | vec![1; n+2] + fill loop |
| Memo sentinel | Hashtbl (no sentinel needed) | vec![vec![-1; len]; len] or HashMap |
| Gap iteration | for gap = 2 to len-1 | for gap in 2..len |
| Overflow risk | OCaml ints (63-bit) | i32 — safe for typical inputs |
Exercises
order table that records the chosen k at each dp[i][j].