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. 为了处理边界情况,在数组首尾添加虚拟气球,值为1
  2. 定义 dp[i][j] 表示戳破开区间 (i,j) 内所有气球能获得的最大硬币数
  3. 枚举区间 (i,j) 内最后戳破的气球 k,此时左右两侧的气球都已被戳破
  4. 状态转移:dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j])

为什么考虑最后戳破而不是最先戳破?

  • 最先戳破会导致子问题之间相互影响,难以独立求解
  • 最后戳破时,其他气球都已消失,边界确定,子问题独立

算法步骤:

  1. 构造新数组,首尾加1
  2. 按区间长度从小到大枚举所有区间
  3. 对每个区间,枚举最后戳破的气球位置
  4. 取所有可能的最大值

时间复杂度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数组存储状态

相关题目