Given n
balloons, indexed from 0
to n-1
. Each balloon is painted with a number on it represented by array nums
. You are asked to burst all the balloons. If the you burst balloon i
you will get nums[left] * nums[i] * nums[right]
coins. Here left
and right
are adjacent indices of i
. After the burst, the left
and right
then becomes adjacent.
Find the maximum coins you can collect by bursting the balloons wisely.
Note:
(1) You may imagine nums[-1] = nums[n] = 1
. They are not real therefore you can not burst them.
(2) 0 ≤ n
≤ 500, 0 ≤ nums[i]
≤ 100
Example:
Given [3, 1, 5, 8]
Return 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
这道题非常有意思,想到了《飞屋环游记》,题目难度为Hard。
看完题目,最直观的想法是用回溯法逐个选择气球引爆,这种方法相当于给所有气球做了一次全排列,时间复杂度会达到O(n!),这么高的时间复杂度大集合测试肯定是过不了的,就不尝试了。
题目复杂的地方在于选择一个气球引爆后分成的子问题会互相关联,如果这样正向思考问题会变得相当复杂。这条路走不通,我们可以逆向思考,依据最后一个气球将问题划分为左右独立的两部分,因为左右两部分的left和right在这种情况下是确定的,所以左右两部分互相独立,这样通过分治法就可以把问题解决了。能否想到这里成为了解决本题的关键,这种逆向思考的方法也是很多问题中常用到的。通过遍历所有的气球,将其逐个作为最后一个引爆的气球计算最后的硬币数,选择其中最大的就得到了最终的结果。同时我们注意到,在采用分治法解决问题的时候,有很多相同的子区间会重复计算到,因此可以把子区间的计算结果缓存起来来优化计算。具体代码:
class Solution {
int getMaxCoins(const vector<int>& nums, vector<vector<int>>& cnt, int bgn, int end, int left, int right) {
if(bgn > end) return 0;
if(cnt[bgn][end]) return cnt[bgn][end];
int maxCn = 0;
for(int i=bgn; i<=end; i++) {
int curCn = 0;
curCn += nums[i]*left*right;
curCn += getMaxCoins(nums, cnt, bgn, i-1, left, nums[i]);
curCn += getMaxCoins(nums, cnt, i+1, end, nums[i], right);
maxCn = max(maxCn, curCn);
}
cnt[bgn][end] = maxCn;
return maxCn;
}
public:
int maxCoins(vector<int>& nums) {
int sz = nums.size();
vector<vector<int>> cnt(sz, vector<int>(sz, 0));
return getMaxCoins(nums, cnt, 0, sz-1, 1, 1);
}
};
既然会有很多重复的子区间,为什么不采用动态规划呢?上面分治法采用自顶向下的方法来分割问题,动态规划则采用自底向上的方法来一步步获取最终的结果。这里我们用二维数组coins来存储优先引爆各区间气球时能获得的最大硬币数,既然优先引爆该区间的气球,那这个区间内最后引爆的气球的left和right就确定了,根据上面同样的方法就可以获得该区间的结果。这样把区间长度从0依次增加到气球数减1,就能够递推出最后的结果。具体代码:
class Solution {
public:
int maxCoins(vector<int>& nums) {
int sz = nums.size(), k = 0;
if(!sz) return 0;
vector<vector<int>> coins(sz, vector<int>(sz, 0));
while(k < sz) {
for(int i=0; i+k<sz; ++i) {
int left = i ? nums[i-1] : 1;
int right = (i+k!=sz-1) ? nums[i+k+1] : 1;
for(int j=i; j<=i+k; ++j) {
int maxCoinsNum = 0;
if(j != i) maxCoinsNum += coins[i][j-1];
if(j != i+k) maxCoinsNum += coins[j+1][i+k];
maxCoinsNum += nums[j]*left*right;
coins[i][i+k] = max(coins[i][i+k], maxCoinsNum);
}
}
++k;
}
return coins[0][sz-1];
}
};
另外看了题目作者的解答,思路差不多,讲解也非常清晰,大家可以看一下,文章最后会给出他的解答链接。代码写的比较简洁,里面增加了去除0的操作,同时在数组首尾加入1,减少了一些判断,大家可以学习下他的代码。具体代码:
int maxCoinsDP(vector<int> &iNums) {
int nums[iNums.size() + 2];
int n = 1;
for (int x : iNums) if (x > 0) nums[n++] = x;
nums[0] = nums[n++] = 1;
int dp[n][n] = {};
for (int k = 2; k < n; ++k) {
for (int left = 0; left < n - k; ++left)
int right = left + k;
for (int i = left + 1; i < right; ++i)
dp[left][right] = max(dp[left][right],
nums[left] * nums[i] * nums[right] + dp[left][i] + dp[i][right]);
}
return dp[0][n - 1];
}
题目作者的解答链接:
https://leetcode.com/discuss/72216/share-some-analysis-and-explanations