戳气球
有 n
个气球,编号为0
到 n - 1
,每个气球上都标有一个数字,这些数字存在数组 nums
中。
现在要求你戳破所有的气球。戳破第 i
个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1]
枚硬币。 这里的 i - 1
和 i + 1
代表和 i
相邻的两个气球的序号。如果 i - 1
或 i + 1
超出了数组的边界,那么就当它是一个数字为 1
的气球。
求所能获得硬币的最大数量。
示例 1:
输入: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
示例 2:
输入:nums = [1,5] 输出:10
提示:
n == nums.length
1 <= n <= 300
0 <= nums[i] <= 100
解题思路
这道题,第一眼看上去没有什么思路,一般来说,求最大值和最小值,都可以用动态规划来处理。
首先我们从条件分析,戳破第i个气球,获取的硬币数是i - 1、i和i + 1三者气球数字的乘积。如果超出了数组的边界,就可以假设为数字为1。所以我们可以在气球左右两边分别增加一个数字为1的气球。代码片段如下:
int n = nums.length;
int[] vals = new int[n+2];
vals[0] = vals[n+1] = 1;
for(int i = 1; i < n + 1; i++){
vals[i] = nums[i-1];
}
我们需要求出下标为1到n的范围内,硬币的最大数量,因为不包含下标0和n + 1,所以我们假设代表的是在开区间之间硬币的最大数量。
接下来,我们肯定是要开始遍历区间的,怎么遍历呢?我们先确定下标 i 和 j ,再定义一个变量k,使得i < k < j
当i、 k、j 相邻时
戳掉下标为k的气球可以获得的硬币数是:
戳气球有哪几种方法呢?
是不是有以下6种情况:
- 按照i、k、j的顺序戳破,得到的硬币数为:
- 按照i、j、k的顺序戳破,得到的硬币数为:
- 按照k、i、j的顺序戳破,得到的硬币数为:
- 按照k、j、i的顺序戳破,得到的硬币数为:
- 按照j、i、k的顺序戳破,得到的硬币数为:
- 按照j、k、i的顺序戳破,得到的硬币数为:
其实答案不是有这6种情况,看我们上面的定义,在开区间,也就是说i、j是边界,所以是不能戳破的,或者说戳破边界能得到的硬币数量是0。所以其实。
这里我们再引申一下,如果i、j是相邻的,那么,因为i、j之间没有可以戳破的气球了;我们后面定义二维数组存放,正好可以把每个值都初始化为0。
当i、k、k2、j相邻时
- 情况1.假设我们先戳破k2,那么
- 情况2.如果我们先戳破k,那么
- 情况3.如果我们把k2和k的位置对换,即i、k2、k、j相邻,先戳破k2,那么
其实可以发现,
在遍历的过程中,情况1的值我们会先存放在dp[ i ][ j ]里,然后和情况3的值比较大小,所以:
根据上面的推断,就可以得到状态转移方程:
当时,
当时,
最后,我们注意一下动态规划的次序,具体代码如下:
class Solution {
public int maxCoins(int[] nums) {
// 动态规划
int n = nums.length;
int[] vals = new int[n+2];
// 数组两侧增加边界
vals[0] = vals[n+1] = 1;
for(int i = 1; i < n + 1; i++){
vals[i] = nums[i-1];
}
int[][] dp = new int[n+2][n+2];
for(int i = n - 1; i >= 0; i--){
for(int j = i + 2; j <= n + 1; j++){
for(int k = i + 1; k < j; k++){
dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k][j] + vals[i] * vals[k] * vals[j]);
}
}
}
return dp[0][n+1];
}
}
复杂度分析
- 时间复杂度:,有三层遍历,每层都是线性的时间复杂度。
- 空间复杂度:,额外定义了一个二维数组。