1、题目描述
https://leetcode-cn.com/problems/burst-balloons/
有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。如果你戳破气球 i ,就可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。
求所能获得硬币的最大数量。
- 你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
- 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
输入: [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、代码详解
(1)新问题定义
每戳破一个气球 nums[i]
,得到的分数和该气球相邻的气球 nums[i-1]
和 nums[i+1]
是有相关性的。
子问题必须独立。对于戳气球问题,想用动态规划,必须巧妙地定义 dp 数组的含义,避免子问题产生相关性。
处理技巧:两个边界加虚拟气球
先直接把这两个边界加进去,形成一个新的数组 points,
现在气球的索引变成了从 1 到 n,points[0] 和 points[n+1] 可以认为是两个「虚拟气球」。
新问题:
在一排气球 points 中,请你戳破气球 0 和气球 n+1 之间的所有气球(不包括 0 和 n+1),使得最终只剩下气球 0 和气球 n+1 两个气球,最多能够得到多少分?
(2)dp定义
- dp[i][j] = x 表示,戳破气球 i 和气球 j 之间(开区间,不包括 i 和 j)的所有气球,可以获得的最高分数为 x
- 结果就是 dp[0][n+1] 的值
- 而 base case 就是 dp[i][j] = 0,0 <= i <= n+1, j <= i+1,开区间 (i, j) 中间根本没有气球可以戳
(3)转移方程
如果正向思考,就只能回溯算法;反向思考,想一想气球 i 和气球 j 之间最后一个被戳破的气球可能是哪一个?所有气球都可能是最后被戳破的那一个,不防假设为 k。
i 和 j 就是两个「状态」,最后戳破的那个气球 k 就是「选择」。
最后一个戳破气球 k
,
- dp[i][j] = dp[i][k] + dp[k][j] + points[i]*points[k]*points[j]
- 先把开区间 (i, k) 的气球都戳破,再把开区间 (k, j) 的气球都戳破;
- 最后剩下的气球 k,相邻的就是气球 i 和气球 j,这时候戳破 k 的话得到的分数就是 points[i]*points[k]*points[j]。
- 那么戳破开区间 (i, k) 和开区间 (k, j) 的气球最多能得到的分数就是 dp[i][k] 和 dp[k][j],这恰好就是对 dp 数组的定义!
由于是开区间,dp[i][k] 和 dp[k][j] 不会影响气球 k;
而戳破气球 k 时,旁边相邻的就是气球 i 和气球 j 了,最后还会剩下气球 i 和气球 j,这也恰好满足了 dp 数组开区间的定义。
对于任一 dp[i][j],希望所有 dp[i][k] 和 dp[k][j] 已经被计算,下图所示:
从下到上从左到右遍历
class Solution(object):
def maxCoins(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
n = len(nums)
# 添加两侧的虚拟气球
points = [0] * (n + 2)
points[0] = points[n + 1] = 1
for i in range(1, n + 1):
points[i] = nums[i - 1]
dp = [[0] * (n + 2) for _ in range(n + 2)] # base case 已经都被初始化为 0
# i 应该从下往上
for i in range(n, -1, -1):
# j 应该从左往右
for j in range(i + 1, n + 2):
# 最后戳破的气球k
for k in range(i + 1, j):
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] +
points[i] * points[j] * points[k])
return dp[0][n + 1]