Hard | LeetCode 312. 戳气球 | 递归+记忆化数组 | 动态规划

312. 戳气球

n 个气球,编号为0n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1i + 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 <= 500
  • 0 <= nums[i] <= 100

解题思路

方法一:递归 + 记忆化搜索

我们观察戳气球的操作,发现这会导致两个气球从不相邻变成相邻,使得后续操作难以处理。
于是我们倒过来看这些操作,将全过程看作是每次添加一个气球。

要在(i, j)区间填满气球的最大的收益是, 首先枚举(i, j)中所有的位置K, 在K处填充一个气球, 得到一个收益nums[i] * nums[j] * nums[k]。然后将(i, j)分成了2部分, 分别是(i, k)和(k, j)。递归的计算这两部分的收益, 然后相加即可。

为了统一边界的处理, 可以在数组的这两个边界分别加上元素1

时间复杂度:O(n^3),其中 n 是气球数量。区间数为 n^2,区间迭代复杂度为 O(n),最终复杂度为 O(n^2 * n) = O(n^3)
空间复杂度:O(n^2),其中 n 是气球数量。缓存大小为区间的个数。

public int[][] rec;
public int[] val;
public int maxCoins(int[] nums) {
    int n = nums.length;
    // 填充两侧边界为1
    val = new int[n + 2];
    for (int i = 1; i <= n; i++) {
        val[i] = nums[i - 1];
    }
    val[0] = val[n + 1] = 1;
    // 初始化记忆化数组
    rec = new int[n + 2][n + 2];
    for (int i = 0; i <= n + 1; i++) {
        Arrays.fill(rec[i], -1);
    }
    // 递归[0, n-1]部分
    return solve(0, n + 1);
}
public int solve(int left, int right) {
    // (left, right) 中间没有可供填充的部分
    if (left >= right - 1) {
        return 0;
    }
    // 返回之前存储过的中间结果
    if (rec[left][right] != -1) {
        return rec[left][right];
    }
    // 遍历(left, right)中间的所有的位置
    for (int i = left + 1; i < right; i++) {
        // 填充第i个位置所获得的收益
        int sum = val[left] * val[i] * val[right];
        // 将(left, right) 分成 (left, i) 和 (i, right)两部分
        // 递归得计算这两部分, 得到把(left, right)这两个位置填充满的收益
        sum += solve(left, i) + solve(i, right);
        // 更新枚举(left, right) 所有位置的产生收益的最大值
        rec[left][right] = Math.max(rec[left][right], sum);
    }
    return rec[left][right];
}

方法二: 动态规划(迭代)

动态规划的迭代版本和上面的递归版本思路是差不多的。不过迭代的过程是从后往前迭代。

设置矩阵rec[][]表示填充(i, j)的所有元素的收益。

那么在(i, j)相等或者相邻的情况下, 收益是为0的。

迭代的方式是第一个for循环 i, 从后往前迭代, 第二个for循环 j, 从i出开始往后迭代。在第三个for循环从前往后枚举(i, j)间的每一个位置。

public int maxCoins(int[] nums) {
    int n = nums.length;
    int[][] rec = new int[n + 2][n + 2];
    int[] val = new int[n + 2];
    val[0] = val[n + 1] = 1;
    for (int i = 1; i <= n; i++) {
        val[i] = nums[i - 1];
    }

    for (int i = n - 1; i >= 0; i--) {
        // j从i+2 开始 让(i, j)恰好有一个中间的空位
        for (int j = i + 2; j <= n + 1; j++) {
            // 尝试 (i, j)之间的所有位置, 找到一个填满 i, j 的最大值
            for (int k = i + 1; k < j; k++) {
                int sum = val[i] * val[k] * val[j];
                sum += rec[i][k] + rec[k][j];
                rec[i][j] = Math.max(rec[i][j], sum);
            }
        }
    }
    return rec[0][n + 1];
}

当然这道题可以换一种迭代的方式, i的迭代方式是从前向后迭代, 第二个for循环j, 从i开始从后往前迭代。第三个for循环从前往后枚举(i, j)的每一个位置。

public int maxCoins(int[] nums) {
    int n = nums.length;
    int[][] rec = new int[n + 2][n + 2];
    int[] val = new int[n + 2];
    val[0] = val[n + 1] = 1;
    for (int i = 1; i <= n; i++) {
        val[i] = nums[i - 1];
    }

    for (int i = 2; i <= n + 1; i++) {
        for (int j = i - 2; j >= 0; j--) {
            for (int k = j + 1; k < i; k++) {
                int sum = val[j] * val[k] * val[i];
                sum += rec[j][k] + rec[k][i];
                rec[j][i] = Math.max(rec[j][i], sum);
            }
        }
    }
    return rec[0][n + 1];
}
原文地址:https://www.cnblogs.com/chenrj97/p/14825386.html