Top 100 Linked Question 修炼------第312题

312. Burst Balloons

题目链接

题目解释

给出n个气球,他们的角标从0到n-1。每个气球上面都画有数字,代表数组中的元素。现在要求你将所有的气球都爆破,如果你选择爆破第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

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

题目分析

首先,按照题目的提示,我们为了避免越界情况的发生,应该先在首尾分别加上元素1,并且保证这两个气球是不能被戳破的。然后我们开始分析,怎么才能获取最大的钱币数。既然出现了最多这个词汇,那么就可以考虑往动态规划(DP)方向去思考,最主要的是建立状态转移方程,下面开始分析过程:

这道题的最难点就是找状态转移方程,还是从定义式来看,假如区间只有一个数,比如 dp[i][i],那么计算起来就很简单,直接乘以周围两个数字即可更新。如果区间里有两个数字,那么就要算两次了,先打破第一个再打破了第二个,或者先打破第二个再打破第一个,比较两种情况,其中较大值就是该区间的dp值。假如区间有三个数呢,比如 dp[1][3],怎么更新呢?如果先打破第一个,剩下两个怎么办呢,难道还要分别再遍历算一下吗?这样跟暴力搜索的方法有啥区别呢,还要 dp 数组有啥意思。所谓的状态转移,就是假设已知了其他状态,来推导现在的状态,现在是想知道 dp[1][3] 的值,那么如果我们先打破了气球1,剩下了气球2和3,若我们之前已经计算了 dp[2][3] 的话,就可以使用其来更新 dp[1][3] 了,就是打破气球1的得分加上 dp[2][3]。那假如我们先打破气球2呢,只要我们之前计算了 dp[1][1] 和 dp[3][3],那么三者加起来就可以更新 dp[1][3]。同理,先打破气球3,就用其得分加上 dp[1][2] 来更新 dp[1][3]。

那么对于有很多数的区间 [i, j],我们如何来更新呢?现在是想知道 dp[i][j] 的值,这个区间可能比较大,但是如果我们知道了所有的小区间的 dp 值,然后聚沙成塔,逐步的就能推出大区间的 dp 值了。还是要遍历这个区间内的每个气球,就用k来遍历吧,k在区间 [i, j] 中,假如第k个气球最后被打爆,那么此时区间 [i, j] 被分成了三部分,[i, k-1],[k],和 [k+1, j],只要我们之前更新过了 [i, k-1] 和 [k+1, j] 这两个子区间的 dp 值,可以直接用 dp[i][k-1] 和 dp[k+1][j],那么最后被打爆的第k个气球的得分该怎么算呢,你可能会下意识的说,就乘以周围两个气球被 nums[k-1] * nums[k] * nums[k+1],但其实这样是错误的,为啥呢?dp[i][k-1] 的意义是什么呢,是打爆区间 [i, k-1] 内所有的气球后的最大得分,此时第 k-1 个气球已经不能用了,同理,第 k+1 个气球也不能用了,相当于区间 [i, j] 中除了第k个气球,其他的已经爆了,那么周围的气球只能是第 i-1 个,和第 j+1 个了,所以得分应为 nums[i-1] * nums[k] * nums[j+1],分析到这里,我们的状态转移方程应该已经跃然纸上了吧,如下所示:

dp[i][j] = max(dp[i][j], nums[i - 1] * nums[k] * nums[j + 1] + dp[i][k - 1] + dp[k + 1][j])                 ( i ≤ k ≤ j )

有了状态转移方程了,我们可以写代码,下面就遇到本题的第二大难点了,区间的遍历顺序。一般来说,我们遍历所有子区间的顺序都是i从0到n,然后j从i到n,然后得到的 [i, j] 就是子区间。但是这道题用这种遍历顺序就不对,在前面的分析中已经说了,我们需要先更新完所有的小区间,然后才能去更新大区间,而用这种一般的遍历子区间的顺序,会在更新完所有小区间之前就更新了大区间,从而不一定能算出正确的dp值,比如拿题目中的那个例子 [3, 1, 5, 8] 来说,一般的遍历顺序是:

[3] -> [3, 1] -> [3, 1, 5] -> [3, 1, 5, 8] -> [1] -> [1, 5] -> [1, 5, 8] -> [5] -> [5, 8] -> [8] 

显然不是我们需要的遍历顺序,正确的顺序应该是先遍历完所有长度为1的区间,再是长度为2的区间,再依次累加长度,直到最后才遍历整个区间:

[3] -> [1] -> [5] -> [8] -> [3, 1] -> [1, 5] -> [5, 8] -> [3, 1, 5] -> [1, 5, 8] -> [3, 1, 5, 8]

我们其实只是更新了 dp 数组的右上三角区域,我们最终要返回的值存在 dp[1][n] 中,其中n是两端添加1之前数组 nums 的个数。参见代码如下:

class Solution:
    def maxCoins(self, nums):
        """
        :param nums:
        :return:
        """
        # 首先将nums首尾均加上1,避免最后乘法判断以及出现越界的情况
        nums = [1] + [n for n in nums if n != 0] + [1]
        # 初始化dp,dp[left][right]代表爆炸掉(left, right)范围(不包含两侧)的所有气球的最大coin数。
        dp = [[0 for i in range(len(nums))] for j in range(len(nums))]

        for balloons_to_burst in range(1, len(nums) - 1):
            # number of balloons in (l,r) region
            for l in range(0, len(nums) - balloons_to_burst - 1):
                # for m and r to be assigned legally
                r = l + balloons_to_burst + 1
                for m in range(l + 1, r):
                    dp[l][r] = max(dp[l][r],dp[l][m] + dp[m][r] + nums[l] * nums[m] * nums[r])
        return dp[0][-1]

总结

2019/6/13 每个动态规划都要推导一遍....

Reference

https://www.cnblogs.com/grandyang/p/5006441.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值