题目描述

n 个气球,编号为0n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。

示例 1:

1
2
3
4
5
输入:nums = [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=0, j=n,我们最后戳破的气球是k,那么最后的到的收益就是nums[i]*nums[k]*nums[j],这是固定的,但是我们需要求出在(i, k)(k, j)之间上次戳破的气球,记作a1, a2,使得nums[i]*nums[a1]*nums[k]nums[k]*nums[a2]*nums[j]再加上刚刚的nums[i]*nums[k]*nums[j]总和最大;很明显,这已经不是一个简单的二分问题,因为你无法直接确定分割点在哪,我们需要枚举每一个可能的k, a1, a2……,因此就想出来这是一道动态规划的题目!

这里借鉴的是评论区题解的一种做法。大致思路是这样的:

  1. 把这个题目倒过来看,假设需要在(i, j)这个区间戳气球

  2. 可以转化为不断添加nums[k], i < k < j,先添加的气球是最后戳破的

  3. 则状态转移就是: dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j])

  4. 这里为了避免nums[i-1]越界,拷贝一个新数组a(n + 2, 1),使得a[i] = nums[i-1]

  5. 特别注意这里是开区间,枚举中间的k

  6. 最后返回dp[0][n+1]即可

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int maxCoins(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));
vector<int> a(n + 2, 1);
for (int i = 1; i <= n; i++) {
a[i] = nums[i-1];
}
for (int len = 3; len <= n+2; len++) {
for (int i = 0; i + len - 1 <= n+1; i++) {
int j = i + len - 1;
for (int k = i + 1; k < j; k++) {
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + a[i] * a[j] * a[k]);
}
}
}
return dp[0][n+1];
}
};