【动态规划-21】312.戳气球

题目描述:

有 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

解题思路:

方法一:动态规划

核心思路:区间 DP

正向思考很难:戳破一个气球后,左右邻居会变化,后续计算依赖当前状态。

反向思考 :考虑最后一个被戳破的气球 。假设最后戳破的是 k,那么:

  • k 左边的气球和右边的气球已经全部戳破

  • k 的邻居是虚拟的 1(数组边界)

  • 最后戳破 k 的收益 = 1 × nums[k] × 1 = nums[k]

但更关键的是 :最后戳破 k 之前,k 把问题分成了左右两个独立的子问题。

状态定义:

在 nums 两边各加一个 1,得到新数组 val,长度为 n + 2。

dp[i][j] = 戳破开区间 (i, j) 内所有气球能获得的最大硬币数。

注意 :i 和 j 是开区间 ,i 和 j 位置的气球不戳破。

状态转移:

枚举 (i, j) 内最后一个被戳破 的气球 k:

复制代码
dp[i][j] = max(dp[i][k] + dp[k][j] + val[i] * val[k] * val[j])
  • dp[i][k]:戳破 (i, k) 内的气球

  • dp[k][j]:戳破 (k, j) 内的气球

  • val[i] * val[k] * val[j]:最后戳破 k 的收益

具体过程示例:

nums = [3,1,5,8],加边界 1 后 val = [1,3,1,5,8,1]

复制代码
dp[0][5] = max(
    dp[0][1] + dp[1][5] + 1*3*1,
    dp[0][2] + dp[2][5] + 1*1*1,
    dp[0][3] + dp[3][5] + 1*5*1,
    dp[0][4] + dp[4][5] + 1*8*1
)

代码实现:

cpp 复制代码
class Solution {
public:
    int maxCoins(vector<int>& nums) {
        int n = nums.size();
        vector<int> val(n + 2, 1);
        for (int i = 0; i < n; i++) {
            val[i + 1] = nums[i];
        }
        
        // dp[i][j] 表示戳破开区间 (i, j) 内所有气球的最大收益
        vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));
        
        // 按区间长度递增遍历
        for (int len = 2; len <= n + 1; len++) {
            for (int i = 0; i + len <= n + 1; i++) {
                int j = i + len;
                
                // 枚举最后一个戳破的气球 k
                for (int k = i + 1; k < j; k++) {
                    dp[i][j] = max(dp[i][j],
                        dp[i][k] + dp[k][j] + val[i] * val[k] * val[j]);
                }
            }
        }
        
        return dp[0][n + 1];
    }
};

复杂度分析:

设 n 是气球数量。

维度 复杂度 说明
时间复杂度 O(n³) 三重循环:区间长度 × 起点 × 分割点
空间复杂度 O(n²) dp 数组

关键细节:

1. 为什么加虚拟边界 1?

处理 i-1 或 i+1 越界的情况,统一为 1,简化边界判断。

2. 为什么用开区间 (i, j)?

因为 dp[i][j] 表示戳破 i 和 j 之间 的气球,i 和 j 本身不戳破。这样 i 和 j 可以作为边界参与乘积计算。

3. 为什么枚举"最后一个戳破"?

因为正向枚举"第一个戳破"会导致左右子问题不独立。反向枚举"最后一个戳破",左右子问题完全独立。

4. 遍历顺序

必须按区间长度递增遍历 ,因为 dp[i][j] 依赖更小的区间 dp[i][k] 和 dp[k][j]。

5. 和「矩阵链乘法」的区别
题目 区别
312. 戳气球 乘积收益
矩阵链乘法 乘积代价

本质都是区间 DP,状态转移类似。

方法二:记忆化搜索

代码实现:

cpp 复制代码
class Solution {
public:
    int maxCoins(vector<int>& nums) {
        int n = nums.size();
        vector<int> val(n + 2, 1);
        for (int i = 0; i < n; i++) val[i + 1] = nums[i];
        
        vector<vector<int>> memo(n + 2, vector<int>(n + 2, -1));
        return dfs(val, 0, n + 1, memo);
    }
    
private:
    int dfs(vector<int>& val, int i, int j, vector<vector<int>>& memo) {
        if (i + 1 == j) return 0;  // 区间内没有气球
        if (memo[i][j] != -1) return memo[i][j];
        
        int result = 0;
        for (int k = i + 1; k < j; k++) {
            result = max(result,
                dfs(val, i, k, memo) + dfs(val, k, j, memo)
                + val[i] * val[k] * val[j]);
        }
        
        memo[i][j] = result;
        return result;
    }
};

复杂度:时间 O(n³),空间 O(n²)

两种方法对比:

方法 时间复杂度 空间复杂度 推荐度
区间 DP O(n³) O(n²) ⭐⭐⭐⭐⭐
记忆化搜索 O(n³) O(n²) ⭐⭐⭐⭐

总结:

要点 说明
核心思想 反向思考,枚举最后一个戳破的气球
状态定义 dp[i][j] 戳破开区间 (i,j) 的最大收益
状态转移 dp[i][j] = max(dp[i][k] + dp[k][j] + val[i]*val[k]*val[j])
时间复杂度 O(n³)
空间复杂度 O(n²)
相关推荐
栈帧之外1 小时前
【C++】STL之set
开发语言·c++
zhanghaha13142 小时前
C++构造函数的一些其他应用(逐词逐句保姆讲解)
c++
jimy13 小时前
c++的 Type Traits (二、更详细版本)
开发语言·c++
碧海银沙音频科技研究院3 小时前
基于炬芯ATS3089X自研AEC声学回声消除算法
算法
通问AI3 小时前
在两台 Mac Studio 上跑 800B 模型:MLX + RDMA over Thunderbolt 实测与四道门选型法
算法
ZywOo6233 小时前
《算法竞赛进阶指南》0x03 前缀和与差分
算法
Scintilla_y4 小时前
DAY2 滑动窗口
算法·leetcode·散列表
lvwangshu4 小时前
笛卡尔树
算法·图论
026d234 小时前
状态信号只知“其然”不知“其所以然”:变工况机械故障诊断的物理盲区
算法