[LeetCode] 312. Burst Balloons

原题链接:https://leetcode.com/problems/burst-balloons/

1. 题目介绍

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

给出一个有n个元素的数组,序号从0开始到n-1。每个元素代表着一个气球,气球上面的数字就是元素的值。每当戳破第 i 个气球,就可以得到nums[left] * nums[i] * nums[right] 个硬币。不可以戳破最边上的气球。
在这里插入图片描述
求最多可以获得的硬币数量是多少?

2. 解题思路

2.1 动态规划

注:本方法及其思路参考了 https://www.cnblogs.com/grandyang/p/5006441.html

2.1.1 构造二维数组

使用二维数组dp,其中dp[ i ][ j ]表示打爆区间[ i , j ]中的所有气球能得到的最多金币。当气球周围没有气球的时候,旁边的数字按1算,这样我们可以在原数组两边各填充一个1,方便计算。

2.1.2 状态转移方程

假如区间[ i , j ] 只有一个数,比如dp[ i ][ i ],那么计算起来就很简单,直接乘以周围两个数字即可。

假如区间[ i , j ]里有两个数字,那么就要算两次了,先打破第一个再打破第二个,或者先打破第二个再打破第一个,比较两种情况,其中较大值就是dp[ i ][ j ]的值。

假如区间[ i , j ]里有很多个数字,假如 k 在区间 [ i, j ] 中,第k个气球先被打爆,然后此时区间 [ i, 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 ],

状态转移方程如下所示:

d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , n u m s [ i − 1 ] ∗ n u m s [ k ] ∗ n u m s [ j + 1 ] + d p [ i ] [ k − 1 ] + d p [ k + 1 ] [ j ] ) dp[i][j] = max(dp[i][j], nums[i - 1] * nums[k] * nums[j + 1] + dp[i][k - 1] + dp[k + 1][j]) dp[i][j]=max(dp[i][j],nums[i1]nums[k]nums[j+1]+dp[i][k1]+dp[k+1][j])
其中 ( i ≤ k ≤ j )

2.1.3 dp数组遍历顺序

有了状态转移方程了,我们可以写代码,下面就遇到本题的第二大难点了,区间的遍历顺序。
一般来说,我们遍历所有子区间的顺序都是 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][length]中,其中length是数组nums的个数。

具体顺序请看下图:

  1. 首先我们先要把打爆长度为1的[ i, j ] 区间里面的气球的值计算出来,此时i = j,并且k = i = j
    在这里插入图片描述
  2. 然后是计算[i , j ]长度为2的区间。此时j = i+1,k = i 或者 i+1
    在这里插入图片描述
  3. 接下来是计算[i , j ]长度为3的区间。此时j = i+2,k = i 或者 i+1 或者i+2
    在这里插入图片描述
  4. 最后计算[i , j ]长度为4的区间。此时j = i+3,k = i 、 i+1 、i+2 或者 i +3
    在这里插入图片描述
    最终,dp[1][length]中存放的就是从引爆第一个气球到第length个气球的最大值。

实现代码

class Solution {
    public int maxCoins(int[] nums) {
		int length = nums.length;
		int newlength = length+2;
		int [] newnums = new int [newlength];
		
		//为头尾添加1,方便计算
		newnums[0] = 1;
		newnums[newlength-1] = 1;
		System.arraycopy(nums, 0, newnums, 1, length);
		//System.arraycopy的作用是:将nums数组里从索引为0的元素开始, 
		//复制到数组newnums里的索引为1的位置, 复制的元素个数为length个. 
		
		
		int[][] dp = new int [newlength][newlength];
		
		//len代表着[i,j]区间的长度,要先从长度为1的区间开始,然后是长度为2的区间
		//最后逐步增加到长度为length的区间。
		for(int len=1 ; len<=length ; len++) {
			for(int i=1 ; i<=length-len+1 ; i++) {
				int j = i+len-1;
				for(int k=i ; k<=j ; k++) {
					int a = dp[i][j];
					int b = dp[i][k-1] + newnums[i-1]*newnums[k]*newnums[j+1] + dp[k+1][j];
					dp[i][j] = Math.max(a, b);
				}
			}
		}
		
        return dp[1][length];
    }
}

3. 参考资料

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值