算法设计与分析:Burst Balloons(Week 6)

学号:16340008

题目:312. Burst Balloons


Question:

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:

  • You may imagine nums[-1] = nums[n] = 1. They are not real therefore you can not burst them.
  • 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100

Example:

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

Answer:

题意是给一串数组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,我们可以得到式子:F(x,y) = F(x,z) + nums[x] *nums[z]*nums[y] + F(z,y)

这个式子有个前提,即删去最后一个数字的时候,增加的result部分必是该子数组的头尾两端和该数字总共三个数字的积。因为我们不能删去这两端(它们可能为元数组的头尾,或为下一步要删的数(正向删除时的下一步))。

有了这条式子,我们只需要套用递归即可得到答案。然而这样算法的效率是很低的。算法的复杂度为O(n2^n),因为我们要把n个数作为最后一个删去的数查找,每个查找都有近O(2^n)的复杂度。如果我们把查找看成n课二叉树,实际上树与树之间以及树自身内有相当多的重复节点,因此我们可以在内存存储已经计算过的F(x,y),这同样也是动态规划里的思想。此时算法的时间复杂度为O(n^3)

于是得到初步代码:

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))

 然而这时候的runtime是非常大的:算法设计与分析:Burst Balloons(Week 6)_第1张图片

然而通过上面的尝试我们可以发现,每次分割,都会尝试其子数组中每个点作为分割点,而子数组的起点和终点也是可以确定的,可以通过起点和长度枚举到所有可能的组合的。因此我们可以得到更线性的结构,而且不需要检验该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]

得到优化效果:

算法设计与分析:Burst Balloons(Week 6)_第2张图片

我们还能讲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]

算法设计与分析:Burst Balloons(Week 6)_第3张图片 

你可能感兴趣的:(算法设计与分析,Python3)