本文主要是介绍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) regionfor l in range(0, len(nums) - balloons_to_burst - 1):# for m and r to be assigned legallyr = l + balloons_to_burst + 1for 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
这篇关于Top 100 Linked Question 修炼------第312题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!