Leetcode Link: 312. 戳气球 - 力扣(LeetCode)

题目

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

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。

解法一

思路:回溯暴搜

非常naive的想法,回溯的时候注意pop掉当前考虑的位置,并且在回溯函数执行完后要insert回来。

题解:超时严重,长度为10的时候就超时了。

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
 
        def process():
            if len(nums) == 0:
                return 0
            res = 1
            for i in range(len(nums)):
                val = nums[i]
                tmp = nums[i]
                if i-1>=0:
                    tmp *= nums[i-1]
                if i+1 <= len(nums)-1:
                    tmp *= nums[i+1]
                nums.pop(i)
                res = max(res, tmp+process())
                nums.insert(i, val)
            return res
 
        return process()

解法二

思路: 从暴力递归到动态规划

先想想这题思考起来为啥感觉很拧巴?

假设我们就正常思考这个问题,我们找了一个 k 位置的气球扎了,结果发现,k-1k+1 相邻了,也就是说每个位置的气球随着我们扎得位置不一样,他的相邻位置会变,就是这个变化让我们无法用一个很自然、很朴素的想法去模拟,所以实现起来会很拧巴。

既然正向不行,那我们索性反着来,只思考我们最后扎哪个气球。 可以预知的是,不考虑实现过程,反过来计算的结果和正着计算结果是一样的(可以举个例子,然后正反计算一下)。

在此之前,我们先在 nums 的前后各添加 1 来防止越界同时可以减少判断,避免逻辑不一致。

nums.insert(0, 1)
nums.append(1)

我们考虑在 (l, r) 的开区间上最后扎哪一个气球,这里假设扎的是 k 位置的气球 (l < k < r),易知此时 res = num[l]*nums[k]*nums[r],即”最后一扎”得到的硬币数。

此时,k(l, r) 分割成了两部分,即 (l, k)(k, r),由于 k 位置的气球我们已经给扎了,所以上面分割出来的两个区间都是开区间。

接下来,问题就变成了,在 (l, k)(k, r) 两个开区间中,我们分别”最后扎的”是哪一个,这样就分成了两个子问题。

实际上这里我们并不会关心在这两个区间里究竟哪个是先扎的,我们只是将问题分解成了两个独立的子问题。

这样,我们就完成了问题的分解,可以用暴力递归的方式解决了

题解

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        nums.append(1)    # 结尾添0
        nums.insert(0, 1) # 开始添0
        
        @cache
        def process(l, r):
            if l == r-1:  # base: 左右边界相邻,实际上相当于没有可以被扎的气球了
                return 0
            if l == r-2:  # base: 左右边界差2,实际上只有中间一个可以扎的气球了
                return nums[l]*nums[r-1]*nums[r]
            
            res = -float('inf') # 提前定义一个最小的答案
            # 寻找在(l, r)开区间内最后一个扎的气球,注意不能取到边界上
            for k in range(l+1, r):
                res = max(res, nums[k]*nums[l]*nums[r]+process(l, k)+process(k, r))
            return res
        
        return process(0, len(nums)-1)

复习时错了,两个base case思考错误。务必注意只有两个球的时候,结果为0。而第二个base case可以不写,在循环体里会计算得到。

解法三

思路:解法二改写动态规划

dp 五步走:

  1. 确定 dp[l][r] 内容:记录 (l, r) 开区间的最大硬币数
  2. 递推公式:见暴力递归
  3. 初始化:if l == r-1: dp[l][r]=0if l == r-2: dp[l][r]=nums[l]*nums[l+1]*nums[r]
  4. 遍历顺序:这个不好直接看出来,看下图吧,结论是行 (l) 逆序,列 (r) 正序
  5. 举例子

通过上图我们易知,顺序就是行倒序,列正序

题解

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        nums.append(1)    # 结尾添0
        nums.insert(0, 1) # 开始添0
        # dp[l][r]
        dp = [[0 for _ in range(len(nums))] for _ in range(len(nums))]
        # 初始化,其实有两个,=0的那个可以省略
        for i in range(len(nums)-2):
            dp[i][i+2] = nums[i]*nums[i+1]*nums[i+2]
 
        for l in range(len(nums)-1, -1, -1):  # 必需反着来
            for r in range(l+3, len(nums)):   # l+1, l+2 都已经初始化了
                res = -float('inf')
                for k in range(l+1, r):
                    res = max(res, nums[k]*nums[l]*nums[r]+dp[l][k]+dp[k][r])
                dp[l][r] = res
        
        return dp[0][len(nums)-1]
        

启发和联系