312. Burst Balloons
原题目链接
题目中文释义
这是一道很好的DP题目,但又不同于传统的DP,以下将自己的思路和别人的想法结合,总结成文。
最初思路——递归
最开始拿到这道题目的时候,想到的最简单粗暴的方法就是递归。完成此题目总共需要n步(戳破n个气球)第一次有n种方式戳破气球,第二次有n-1种方式……以此类推,算出每一种戳气球的方式所得的金币,然后返回最大值。这样的总共有n!种方式,时间复杂度为O(n!),显然是我们无法接受的。所以逐步优化。
DP引入
接下来可以这样考虑:如果我们设剩余的气球数为N,已经戳破的气球数为M。我们可以发现maxCoin(N)与M是无关的。也就是已经戳破的气球不会影响后面气球所能得到的最大值。这可以让我们想到DP中的无后效性。于是我们可以尝试DP(bottom up),即先计算戳破2个气球的maxCoin,再计算戳破3个气球的maxCoin,以此类推知道我们算出n个气球的maxCoin(重叠子问题)。这种方法比O(n!)好但并不优于O(2^n),我们需要进一步改进寻找多项式级别的算法。
最终解法
有了之前的想法,我们接下来准备用DP对问题进一步优化。一般而言,我们习惯性的从中选出一个数,然后算出剩余数字所能得到的最大值,这样可以得到与原问题相似的子问题。
大概的公式如下:
所能得到的最大分数 = max(取出一个数字的分数+余下数列所能得到的最大分数)
这是我们一般的DP思路,一般而言通过循环遍历就可以依次得出子问题的答案,从而最终获得正确答案。但当我们这样尝试的时候马上就会发现问题:取出的数字得分与左右两个相邻数字有关,同时任意操作会改变左右的相邻关系
我们进一步阐述一下这个问题:
比如你有一个数列[a1,a2,a3...an]
,你想通过选择一个点k
将其分为两部分,我们设为N1,N2
。我们戳破k
所在的气球,将得到N1 = [a1,a2,...,ak-1]
和 N2 = [ak+1,ak+2,...,an]
。问题在于,根据计算规则,k气球破了之后,a(k-1)和a(k+1)会变成相邻的,如果此时踩a(k-1)或者a(k+1),则都会受到另一个子整体的影响。换言之,我们分割出来的两个子问题不是相互独立的,所以这样分割是行不通的。但DP的方向还是没错的,我们接下来的任务是确定k
所在的位置,从而使分出来的两个子问题可以相互独立,从而进一步确定转移方程。
这里的关键思想在于:如果数列中只有一个数,或者说最后只剩下一个数(a[k])时,我们是知道它的分数的:
1*a[k]*1
或者 nums[-1]*a[k]*nums[n]
如果说我们设k是最后一个被戳破的气球,那么在这之前,N1 = [a1,a2,...,ak-1]
和 N2 = [ak+1,ak+2,...,an]
就有了非常明确的边界,也就实现了两者相互独立的目标。
这样,我们得出的转移方程如下:
dp[left][right]=max(dp[left][right],nums[left]*nums[k]*nums[right]+dp[left][k]+dp[k][right])
代码
class Solution {
public:
int maxCoins(vector<int>& nums) {
vector<int> temp(nums.size()+2,0);
for(int i = 0; i < nums.size(); i++)
temp[i+1] = nums[i];
temp[0] = temp[nums.size()+1] = 1;
int n = temp.size();
vector<vector<int>> dp(n,vector<int>(n,0));
for(int d = 2; d < n ; d++){
for(int left = 0; left < n-d; left++){
int right = left + d;
for(int i = left+1; i < right; i++)
dp[left][right] = max(dp[left][right], temp[left]*temp[i]*temp[right]+dp[left][i]+dp[i][right]);
}
}
return dp[0][n-1];
}
};