Leetcode: 戳气球

42 阅读3分钟

原题:leetcode.cn/problems/bu…

题目描述

有 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. 动态规划(区间DP)
    • 定义 dp[i][j] 为戳破区间 (i, j) 内气球能获得的最大硬币数
    • 区间开区间处理,不包含边界气球
  2. 倒序思考
    • 考虑区间内最后戳破的气球 k
    • 戳破 k 时获得 nums[i]*nums[k]*nums[j] 硬币
  3. 状态转移
    • dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j])
    • 枚举所有可能的 k
  4. 边界处理
    • 在数组两端添加虚拟气球 [1, ..., 1]
    • 初始化长度为0的区间值为0

关键洞察

  1. 动态规划(区间DP)
    • 定义 dp[i][j] 为戳破区间 (i, j) 内气球能获得的最大硬币数
    • 区间开区间处理,不包含边界气球
  2. 倒序思考
    • 考虑区间内最后戳破的气球 k
    • 戳破 k 时获得 nums[i]*nums[k]*nums[j] 硬币
  3. 状态转移
    • dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j])
    • 枚举所有可能的 k
  4. 边界处理
    • 在数组两端添加虚拟气球 [1, ..., 1]
    • 初始化长度为0的区间值为0

复杂度分析

指标说明
时间复杂度O(n³):三重循环(长度、起点、分割点)
空间复杂度O(n²):二维DP表存储区间最优解
最优情况O(n³):无法低于立方复杂度

代码实现

  • JavaScript(区间DP)
const maxCoins = (nums) => {
    const n = nums.length;
    // 添加首尾虚拟气球
    const balloons = [1, ...nums, 1];
    const m = balloons.length;

    // 初始化DP表
    const dp = Array.from({ length: m }, () => Array(m).fill(0));

    // 按区间长度枚举
    for (let len = 2; len < m; len++) {
        for (let i = 0; i < m - len; i++) {
            const j = i + len;
            // 枚举最后戳破的气球k
            for (let k = i + 1; k < j; k++) {
                const coins = dp[i][k] + dp[k][j] + 
                            balloons[i] * balloons[k] * balloons[j];
                dp[i][j] = Math.max(dp[i][j], coins);
            }
        }
    }

    return dp[0][m - 1];
};
  • JavaScript(回溯)
/**
 * @param {number[]} nums
 * @return {number}
 */
var maxCoins = function (nums) {
    const n = nums.length;
    const visited = Array.from({ length: n }, () => []);

    const helper = (begin, end) => {
        if (begin > end) {
            return 0;
        }

        if (visited[begin][end]) {
            return visited[begin][end]
        }

        let ans = 0;
        for (let i = begin; i <= end; i++) {
            let cur = nums[i];

            const left = helper(begin, i - 1);
            const right = helper(i + 1, end);

            if (begin - 1 >= 0) cur *= nums[begin - 1];
            if (end + 1 < n) cur *= nums[end + 1]

            ans = Math.max(ans, left + right + cur)
        }

        visited[begin][end] = ans;
        return ans;
    }

    return helper(0, n - 1);
};
  • 变种:输出最有戳破顺序
const maxCoinsWithPath = (nums) => {
    const balloons = [1, ...nums, 1];
    const n = balloons.length;
    const dp = Array.from({ length: n }, () => Array(n).fill(0));
    const path = Array.from({ length: n }, () => Array(n).fill(-1));

    for (let len = 2; len < n; len++) {
        for (let i = 0; i < n - len; i++) {
            const j = i + len;
            for (let k = i + 1; k < j; k++) {
                const coins = dp[i][k] + dp[k][j] + balloons[i] * balloons[k] * balloons[j];
                if (coins > dp[i][j]) {
                    dp[i][j] = coins;
                    path[i][j] = k; // 记录最后戳破的位置
                }
            }
        }
    }

    // 回溯重建路径
    const order = [];
    const buildPath = (i, j) => {
        if (i + 1 === j) return;
        const k = path[i][j];
        order.push(balloons[k]);
        buildPath(i, k);
        buildPath(k, j);
    };

    buildPath(0, n - 1);
    return { coins: dp[0][n - 1], order };
};

实际应用场景

  1. 最优拆除规划:建筑物拆除顺序优化(相邻建筑影响)`
  2. 化学链反应:分子链断裂能量最大化计算`
  3. 游戏策略:泡泡龙类游戏的最佳消除顺序`
  4. 生产流程:工序依赖型生产线的任务排序`

相似题目