学号:16340008
题目:312. Burst Balloons
Given n
balloons, indexed from 0
to n-1
. Each balloon is painted with a number on it represented by array nums
. You are asked to burst all the balloons. If the you burst balloon i
you will get nums[left] * nums[i] * nums[right]
coins. Here left
and right
are adjacent indices of i
. After the burst, the left
and right
then becomes adjacent.
Find the maximum coins you can collect by bursting the balloons wisely.
Note:
nums[-1] = nums[n] = 1
. They are not real therefore you can not burst them.n
≤ 500, 0 ≤ nums[i]
≤ 100Example:
Input:[3,1,5,8]
Output:167 Explanation:
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
题意是给一串数组nums,和一个值为0的result,每次删去其中一个数nums[i],result += nums[i-1] * nums[i] * nums[i+1](假设nums[-1] = nums[sizeof(nums)] = 1)。考虑题目是在Divide and Conquer分类中找到的,因此首先考虑分治法。然而这串数组不能简单的分治,因为每一次删去都会对数组产生影响,导致后续步骤得出结果的变化。如果我们从第一个删去的数开始考虑,我们无法在子问题中决定数的左右。
这里我们需要用到动态规划的思想(实际上做这题的时候看了不少Discuss的内容,并自行了解动态规划的思想后才理解),逆向考虑。此题我们从最后一个删去的数考虑。假如二元函数F(x,y)表示子数组串nums[x]到nums[y]能得到的最大result。假如当nums[z](x < z < y)是这串数组中最后一个删去时,能得到最大result,我们可以得到式子:
这个式子有个前提,即删去最后一个数字的时候,增加的result部分必是该子数组的头尾两端和该数字总共三个数字的积。因为我们不能删去这两端(它们可能为元数组的头尾,或为下一步要删的数(正向删除时的下一步))。
有了这条式子,我们只需要套用递归即可得到答案。然而这样算法的效率是很低的。算法的复杂度为O(),因为我们要把n个数作为最后一个删去的数查找,每个查找都有近O()的复杂度。如果我们把查找看成n课二叉树,实际上树与树之间以及树自身内有相当多的重复节点,因此我们可以在内存存储已经计算过的F(x,y),这同样也是动态规划里的思想。此时算法的时间复杂度为O()
于是得到初步代码:
class Solution:
def maxCoins(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
self.dp = []
self.nums = [1] + nums + [1]
n = len(self.nums)
for _ in range(n):
row = []
for _ in range(n):
row.append(0)
self.dp.append(row)
return self.div(0, n-1)
def div(self, x, y):
if self.dp[x][y] or x == y - 1:
return self.dp[x][y]
for z in range(x+1, y):
self.dp[x][y] = max(self.dp[x][y], self.nums[x] * self.nums[z] * self.nums[y] + self.div(x, z) + self.div(z, y))
return self.dp[x][y]
其中dp[x][y]相当于上文提到的F(x,y),是左右边界为x,y的情况下以其中某点分割得到的最大result。
测试用代码如下:
test = Solution()
nums = [3,1,5,8]
print(test.maxCoins(nums))
然而通过上面的尝试我们可以发现,每次分割,都会尝试其子数组中每个点作为分割点,而子数组的起点和终点也是可以确定的,可以通过起点和长度枚举到所有可能的组合的。因此我们可以得到更线性的结构,而且不需要检验该dp[x][y]是否有存储过。显然我们需要从x与y间距(简称长度)最短的dp[x][y](最短子数组)开始赋值,起点从数组头移动直到终点到极限,然后长度加1,开始下一轮的起点移动。然后我们可以再优化dp二维数组的初始化。最终得到以下代码:
class Solution:
def maxCoins(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
self.dp = []
self.nums = [1] + nums + [1]
n = len(self.nums)
self.dp = [[0] * n for _ in range(n)]
for l in range(2, n):
for x in range(n - l):
y = x + l
for z in range(x + 1, y):
self.dp[x][y] = max(self.dp[x][y], self.nums[x] * self.nums[z] * self.nums[y] + self.dp[x][z] + self.dp[z][y])
return self.dp[0][n - 1]
得到优化效果:
我们还能讲self.删去,则不必增加类中成员,只在函数中有临时变量,最终提速效果也十分明显:
class Solution:
def maxCoins(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
dp = []
nums = [1] + nums + [1]
n = len(nums)
dp = [[0] * n for _ in range(n)]
for l in range(2, n):
for x in range(n - l):
y = x + l
for z in range(x + 1, y):
dp[x][y] = max(dp[x][y], nums[x] * nums[z] * nums[y] + dp[x][z] + dp[z][y])
return dp[0][n - 1]