963 字
5 分钟
戳气球

题目描述
有 n
个气球,编号为0
到 n - 1
,每个气球上都标有一个数字,这些数字存在数组 nums
中。
现在要求你戳破所有的气球。戳破第 i
个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1]
枚硬币。 这里的 i - 1
和 i + 1
代表和 i
相邻的两个气球的序号。如果 i - 1
或 i + 1
超出了数组的边界,那么就当它是一个数字为 1
的气球。
求所能获得硬币的最大数量。
解题思路
- 动态规划(区间DP):
- 定义
dp[i][j]
为戳破区间(i, j)
内气球能获得的最大硬币数 - 区间开区间处理,不包含边界气球
- 定义
- 倒序思考:
- 考虑区间内最后戳破的气球
k
- 戳破
k
时获得nums[i]*nums[k]*nums[j]
硬币
- 考虑区间内最后戳破的气球
- 状态转移:
dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j])
- 枚举所有可能的
k
- 边界处理:
- 在数组两端添加虚拟气球
[1, ..., 1]
- 初始化长度为0的区间值为0
- 在数组两端添加虚拟气球
关键洞察
- 动态规划(区间DP):
- 定义
dp[i][j]
为戳破区间(i, j)
内气球能获得的最大硬币数 - 区间开区间处理,不包含边界气球
- 定义
- 倒序思考:
- 考虑区间内最后戳破的气球
k
- 戳破
k
时获得nums[i]*nums[k]*nums[j]
硬币
- 考虑区间内最后戳破的气球
- 状态转移:
dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j])
- 枚举所有可能的
k
- 边界处理:
- 在数组两端添加虚拟气球
[1, ..., 1]
- 初始化长度为0的区间值为0
- 在数组两端添加虚拟气球
复杂度分析
指标 | 说明 |
---|---|
时间复杂度 | O(n³):三重循环(长度、起点、分割点) |
空间复杂度 | O(n²):二维DP表存储区间最优解 |
最优情况 | O(n³):无法低于立方复杂度 |
代码实现
区间 DP
const maxCoins = (nums) => { const n = nums.length; // 添加首尾虚拟气球 const balloons = [1, ...nums, 1]; const m = balloons.length;
// 初始化DP表 const dp = Array.from({ length: m }, () => Array(m).fill(0));
// 按区间长度枚举 for (let len = 2; len < m; len++) { for (let i = 0; i < m - len; i++) { const j = i + len; // 枚举最后戳破的气球k for (let k = i + 1; k < j; k++) { const coins = dp[i][k] + dp[k][j] + balloons[i] * balloons[k] * balloons[j]; dp[i][j] = Math.max(dp[i][j], coins); } } }
return dp[0][m - 1];};
回溯
/** * @param {number[]} nums * @return {number} */var maxCoins = function (nums) { const n = nums.length; const visited = Array.from({ length: n }, () => []);
const helper = (begin, end) => { if (begin > end) { return 0; }
if (visited[begin][end]) { return visited[begin][end] }
let ans = 0; for (let i = begin; i <= end; i++) { let cur = nums[i];
const left = helper(begin, i - 1); const right = helper(i + 1, end);
if (begin - 1 >= 0) cur *= nums[begin - 1]; if (end + 1 < n) cur *= nums[end + 1]
ans = Math.max(ans, left + right + cur) }
visited[begin][end] = ans; return ans; }
return helper(0, n - 1);};
实际应用场景
- 最优拆除规划:建筑物拆除顺序优化(相邻建筑影响)`
- 化学链反应:分子链断裂能量最大化计算`
- 游戏策略:泡泡龙类游戏的最佳消除顺序`
- 生产流程:工序依赖型生产线的任务排序`
变种:输出最优戳破顺序
const maxCoinsWithPath = (nums) => { const balloons = [1, ...nums, 1]; const n = balloons.length; const dp = Array.from({ length: n }, () => Array(n).fill(0)); const path = Array.from({ length: n }, () => Array(n).fill(-1));
for (let len = 2; len < n; len++) { for (let i = 0; i < n - len; i++) { const j = i + len; for (let k = i + 1; k < j; k++) { const coins = dp[i][k] + dp[k][j] + balloons[i] * balloons[k] * balloons[j]; if (coins > dp[i][j]) { dp[i][j] = coins; path[i][j] = k; // 记录最后戳破的位置 } } } }
// 回溯重建路径 const order = []; const buildPath = (i, j) => { if (i + 1 === j) return; const k = path[i][j]; order.push(balloons[k]); buildPath(i, k); buildPath(k, j); };
buildPath(0, n - 1); return { coins: dp[0][n - 1], order };};