题目
有 n
个气球,编号为 0
到 n-1
,每个气球上都标有一个数字,这些数字存在数组 nums
中。
现在要求你戳破所有的气球。如果你戳破气球
i
,就可以获得nums[left] * nums[i] * nums[right]
个硬币。 这里的left
和right
代表和i
相邻的两个气球的序号。注意当你戳破了气球i
后,气球left
和气球right
就变成了相邻的气球。
求所能获得硬币的最大数量。
说明:
- 你可以假设
nums[-1] = nums[n] = 1
,但注意它们不是真实存在的所以并不能被戳破。 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
示例:
输入: [3,1,5,8]
输出: 167
解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
解题思路
我们先定义开区间(i, j)
表示只能戳破 i
和 j
之间的气球,不能戳 i
和 j
。以示例为例,画出它的(0, 5)
开区间图。
所以整个思路就是不管前面的气球怎么戳破的,只关注当前开区间内,最后戳破的气球是哪个。假设目前存在一个开区间(i, j)
,最后一个
要戳破的气球是 k
,此时的开区间图可以画成如下。
从这里可以看出,戳破 k
的时候,只与开区间的边界 i
和 j
(无法戳破)的气球有关,与开区间(i,k)
和(k,j)
的戳破情况完全无关。所以我们的关注点只有最后一个戳破的气球
。
此时假设 dp[i][j]
表示开区间(i,j)
内能获得的最大硬币数,那么(i,j)
开区间得到的金币可以由 dp[i][k]
和 dp[k][j]
进行状态转移,若最后一个戳爆的气球为 k
,能获得的硬币数:sum = dp[i][k] + val[i] * val[k] * val[j] + dp[k][j]
val[i]
表示i
位置气球的数字
最终状态转移方程如下:
一个小技巧, 令
val[i] = nums [i - 1]
,可以防止数组越界,减少边界条件处理。
代码实现
// 动态规划
var maxCoins = function (nums) {
const len = nums.length;
// 构造val数组,另val[i] = nums[i - 1]
const val = new Array(len + 2);
val[0] = 1;
val[len + 1] = 1;
for (let i = 1; i <= len; ++i) {
val[i] = nums[i - 1];
}
// 初始化dp数组,其值全为0
const dp = new Array(len + 2);
for (let i = 0; i < len + 2; ++i) {
dp[i] = new Array(len + 2).fill(0);
}
// 开区间的边界有一侧需要倒序遍历,否则无法覆盖所有区间,这里选择左侧倒序
for (let i = len - 1; i >= 0; --i) {
for (let j = i + 2; j <= len + 1; ++j) {
for (let k = i + 1; k < j; ++k) {
// 状态转移核心
let sum = val[i] * val[k] * val[j];
sum += dp[i][k] + dp[k][j];
dp[i][j] = Math.max(dp[i][j], sum);
}
}
}
//
return dp[0][len + 1];
};
复杂度分析
时间复杂度: O ( n 3 ) O(n^3) O(n3),其中 n
是气球数量。状态数为 n 2 n^2 n2,状态转移复杂度为 O(n)
,最终复杂度为 O ( n 2 ∗ n ) = O ( n 3 ) O(n^2*n)=O(n^3) O(n2∗n)=O(n3)
空间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n
是气球数量。