leetcode
leetcode 251 ~ 300
戳气球

戳气球

难度:

标签:

题目描述

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 <= 300
  • 0 <= nums[i] <= 100

代码结果

运行时间: 1095 ms, 内存: 17.9 MB


/*
 * 思路:
 * 1. 使用动态规划的方式解决该问题,但是使用Java Stream来进行数据的处理。
 * 2. 依旧是使用 dp 数组来存储每个区间内的最大硬币数。
 * 3. 通过Stream API进行数据流式处理,代码会更加简洁和直观。
 */
import java.util.Arrays;
 
public class BurstBalloonsStream {
    public int maxCoins(int[] nums) {
        int n = nums.length;
        int[] newNums = new int[n + 2];
        newNums[0] = 1;
        newNums[n + 1] = 1;
        System.arraycopy(nums, 0, newNums, 1, n);
 
        int[][] dp = new int[n + 2][n + 2];
        Arrays.stream(dp).forEach(row -> Arrays.fill(row, 0));
 
        for (int length = 1; length <= n; length++) {
            for (int left = 1; left <= n - length + 1; left++) {
                int right = left + length - 1;
                int l = left, r = right;
                dp[left][right] = Arrays.stream(newNums, left, right + 1)
                    .map(k -> dp[left][k - 1] + newNums[left - 1] * k * newNums[right + 1] + dp[k + 1][right])
                    .max()
                    .orElse(0);
            }
        }
        return dp[1][n];
    }
}

解释

方法:

这个题解使用动态规划来解决戳气球问题。主要思路是:考虑最后戳破的气球是哪个,假设是第 k 个气球,那么状态转移方程就是 dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]),其中 dp[i][j] 表示戳破 (i, j) 范围内的所有气球能得到的最大硬币数,k 是 (i, j) 范围内最后一个戳破的气球。根据状态转移方程写出代码即可。

时间复杂度:

O(n^3)

空间复杂度:

O(n^2)

代码细节讲解

🦆
为什么在解题时选择动态规划方法而非贪心或回溯算法?
选择动态规划方法是因为戳气球问题涉及到多个选择与决策的顺序,这些决策之间存在重叠的子问题,并且具有最优子结构特性。贪心算法虽然在某些步骤可能找到局部最优解,但它不能保证全局最优解,因为戳破一个气球的决策会影响后续的选择。回溯算法能够遍历所有可能的选择,但其时间复杂度通常非常高,不适合解决问题规模较大的情况。动态规划通过建立状态转移方程,逐步求解,避免了重复计算,因此在时间和空间效率上更优。
🦆
在动态规划的过程中,如何确保所有情况都被考虑到,特别是在选择最后一个戳破的气球k时?
在动态规划过程中,确保所有情况都被考虑到的关键是通过妥善的循环结构来遍历所有可能的状态。对于每一对(i, j),我们考虑所有可能的k(i < k < j),使得k是(i, j)区间内最后一个被戳破的气球。通过从较小的子区间开始计算,逐步扩展到更大的子区间,我们可以确保在计算dp[i][j]时,dp[i][k]和dp[k][j]已经被计算过,从而保证了计算的正确性和完整性。
🦆
动态规划数组`dp`的初始化为什么是0,这是否意味着在任何开始和结束位置之间没有气球的情况下硬币数为0?
是的,dp数组初始化为0的确是因为在任何开始(i)和结束(j)位置之间,如果没有气球存在(即i直接连接到j,中间没有k可以选择),那么该区间能够获得的硬币数自然为0。这种初始化方式简化了边界条件的处理,并且符合问题的逻辑,即没有气球可以戳破时,收益为零。
🦆
题解中提到加入两个值为1的气球来简化边界条件,这种处理方式是否会影响最终的最大硬币计算?
加入两个值为1的气球是为了简化边界条件处理,这些边界气球不会影响最终计算的结果。加入这些气球后,我们可以假设所有的戳气球操作都是在一个完整的框架内执行的,即每个子问题的边界都有气球存在。这样可以避免在实现时对边界情况进行特殊处理,简化代码逻辑。实际上,这些边界气球的戳破只会在最初和最后发生,且它们的值设为1,所以对于计算其他气球带来的硬币总数没有直接影响。

相关问题

合并石头的最低成本

n 堆石头排成一排,第 i 堆中有 stones[i] 块石头。

每次 移动 需要将 连续的 k 堆石头合并为一堆,而这次移动的成本为这 k 堆中石头的总数。

返回把所有石头合并成一堆的最低成本。如果无法合并成一堆,返回 -1

 

示例 1:

输入:stones = [3,2,4,1], K = 2
输出:20
解释:
从 [3, 2, 4, 1] 开始。
合并 [3, 2],成本为 5,剩下 [5, 4, 1]。
合并 [4, 1],成本为 5,剩下 [5, 5]。
合并 [5, 5],成本为 10,剩下 [10]。
总成本 20,这是可能的最小值。

示例 2:

输入:stones = [3,2,4,1], K = 3
输出:-1
解释:任何合并操作后,都会剩下 2 堆,我们无法再进行合并。所以这项任务是不可能完成的。.

示例 3:

输入:stones = [3,5,1,2,6], K = 3
输出:25
解释:
从 [3, 5, 1, 2, 6] 开始。
合并 [5, 1, 2],成本为 8,剩下 [3, 8, 6]。
合并 [3, 8, 6],成本为 17,剩下 [17]。
总成本 25,这是可能的最小值。

 

提示:

  • n == stones.length
  • 1 <= n <= 30
  • 1 <= stones[i] <= 100
  • 2 <= k <= 30