Hard
题目描述
给你 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
解题思路
这是一道经典的区间动态规划题目。关键在于转换思路:不要考虑先戳破哪个气球,而要考虑最后戳破哪个气球。
核心思路:
- 为了处理边界情况,在数组首尾添加虚拟气球,值为1
- 定义
dp[i][j]表示戳破开区间(i,j)内所有气球能获得的最大硬币数 - 枚举区间
(i,j)内最后戳破的气球 k,此时左右两侧的气球都已被戳破 - 状态转移:
dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j])
为什么考虑最后戳破而不是最先戳破?
- 最先戳破会导致子问题之间相互影响,难以独立求解
- 最后戳破时,其他气球都已消失,边界确定,子问题独立
算法步骤:
- 构造新数组,首尾加1
- 按区间长度从小到大枚举所有区间
- 对每个区间,枚举最后戳破的气球位置
- 取所有可能的最大值
时间复杂度O(n³),空间复杂度O(n²)。这是该问题的最优解法。
代码实现
class Solution {
public:
int maxCoins(vector<int>& nums) {
int n = nums.size();
vector<int> arr(n + 2, 1);
for (int i = 0; i < n; i++) {
arr[i + 1] = nums[i];
}
vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));
for (int len = 2; len <= n + 1; len++) {
for (int i = 0; i <= n + 1 - len; i++) {
int j = i + len;
for (int k = i + 1; k < j; k++) {
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + arr[i] * arr[k] * arr[j]);
}
}
}
return dp[0][n + 1];
}
};
class Solution:
def maxCoins(self, nums: List[int]) -> int:
n = len(nums)
arr = [1] + nums + [1]
dp = [[0] * (n + 2) for _ in range(n + 2)]
for length in range(2, n + 2):
for i in range(n + 2 - length):
j = i + length
for k in range(i + 1, j):
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + arr[i] * arr[k] * arr[j])
return dp[0][n + 1]
public class Solution {
public int MaxCoins(int[] nums) {
int n = nums.Length;
int[] arr = new int[n + 2];
arr[0] = arr[n + 1] = 1;
for (int i = 0; i < n; i++) {
arr[i + 1] = nums[i];
}
int[,] dp = new int[n + 2, n + 2];
for (int len = 2; len <= n + 1; len++) {
for (int i = 0; i <= n + 1 - len; i++) {
int j = i + len;
for (int k = i + 1; k < j; k++) {
dp[i, j] = Math.Max(dp[i, j], dp[i, k] + dp[k, j] + arr[i] * arr[k] * arr[j]);
}
}
}
return dp[0, n + 1];
}
}
var maxCoins = function(nums) {
const n = nums.length;
const arr = [1, ...nums, 1];
const dp = Array(n + 2).fill(0).map(() => Array(n + 2).fill(0));
for (let len = 2; len <= n + 1; len++) {
for (let i = 0; i <= n + 1 - len; i++) {
const j = i + len;
for (let k = i + 1; k < j; k++) {
dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k][j] + arr[i] * arr[k] * arr[j]);
}
}
}
return dp[0][n + 1];
};
复杂度分析
| 复杂度类型 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n³) | 三重循环:区间长度、起始位置、最后戳破位置 |
| 空间复杂度 | O(n²) | 二维DP数组存储状态 |