题目描述
有 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³):无法低于立方复杂度 |
代码实现
- JavaScript(区间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];
};
- JavaScript(回溯)
/**
* @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 };
};
实际应用场景
- 最优拆除规划:建筑物拆除顺序优化(相邻建筑影响)`
- 化学链反应:分子链断裂能量最大化计算`
- 游戏策略:泡泡龙类游戏的最佳消除顺序`
- 生产流程:工序依赖型生产线的任务排序`