题目描述
给定$n$个气球,标号从$0$到$n-1$。每一个气球上都画着一个数字,这组数字用数组$nums[]$表示。
要戳爆所有气球。如果戳气球$i$,那么会得到$nums[left] * nums[i] * nums[right]$个金币。这里$left$和$right$是$i$相邻的索引。戳完气球$i$,气球$left$和气球$right$会相邻。
目标是收集到最多的金币,通过选择最好的戳气球顺序。
注意:设定$num[-1] = nums[n] = 1, 0 <= n <= 500, 0 <= nums[i] <= 100$。
样例
Example 1:
输入: [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
算法1
(动态规划) $O(n^3)$
状态: $dp[i][j]$表示戳爆从第$i$到第$j$个气球得到的最大金币数。
状态转移方程: $ dp[i][j] = max( dp[i][j], dp[i][k-1] + num[i-1] * nums[k] * nums[j+1] + dp[k+1][j] )$
其中,$k$可以理解成$[i,j]$范围里最后戳破的一个气球。
时间复杂度$O(n^3)$: 三层循环
空间复杂度$O(n^2)$: $dp[i][j]$数组的大小是$(n+2) * (n+2)$
C++ 代码
class Solution {
public:
int maxCoins(vector<int>& nums) {
int n = nums.size();
nums.insert(nums.begin(),1); //在nums最前面插入一个1,表示nums[-1]=1
nums.push_back(1); //在nums最后面插入一个1,表示nums[n]=1
vector<vector<int>> dp(n+2, vector<int>(n+2, 0)); //初始化dp数组
for(int len=1; len<=n; ++len){
for(int i=1; i<=n-len+1; ++i){
int j = i+len-1;
for(int k=i; k<=j; ++k){
dp[i][j] = max(dp[i][j], dp[i][k-1] + nums[i-1]*nums[k]*nums[j+1] + dp[k+1][j]);
}
}
}
return dp[1][n];
}
};
k值的理解太关键了,终于懂了。w~w
Yes !!! 我也卡在k值是什么半天了
如果
k
表示的是最后一个戳破的气球,怎么确定最后第k个气球的旁边一定是num[i-1]
和num[j+1]
呢?k
表示的是区间[i, j]
间最后一个戳破的气球,i <= k <= j
感觉可以这样想,在剩
k
最后这一个气球前,我们已经把区间[i, j]
内所有其他的气球都戳破了,那么此时k
气球左右两边的为i-1
和j+1
气球为什么是
nums[i-1]*nums[k]*nums[j+1]
呢写得太好了
请问为什么f(i,j)=max(f(i,j),…)呢,(i,j)的状态应该只被计算一次,直接f(i,j)=…不就可以吗。
会被计算多次的,每次长度不同
不是每次长度不同 是每次的K 最后点击的气球不同,要一直比较才能得出最大的 f[i][j]
@yxc 请问如何确定边界的?
for(int i=1; i<=n-len+1; ++i)
和
int j = i+len-1;
这是枚举区间的惯用手法
先枚举区间长度
len
,然后枚举起点i
, 重点j
自然而然就定下来得了j = i + len -1
为什么o(n^3) 是可以的呢?看题目的时候怎么知道这样不会running time exceed呢
超时与否取决于最终的计算量,时间复杂度只是提供一个参考,因为相同复杂度的算法常数可能差别非常大。
评测器的时限一般是1妙,在C++中可容许的计算次数大约是 $10^8$,此题中算法复杂度是 $O(n^3)$,但三重循环的次数大约是 $n^3/6$,由于 $n \le 500$,所以计算量大约是 $2 \times 10^7$,1秒内可以出解。