思路:
在使用分治法时,我们应该考虑的核心问题是如何用子问题的解来表示原问题的解,也就是子问题该如何划分才能通过子问题来求解原问题。我们把描述子问题的解与原问题的解之间的关系的表达式称为状态转移方程。
首先我们尝试每戳破一个气球,以该气球为边界将气球数组分为两部分,使用这两部分的解来求解原问题。
我们设戳破区间 i 到 j 间的气球我们得到的最大金币数为coin。及coin = def( i , j )。
则当我们戳破气球 k 时,两边区间的最大值分别是 def( i , k-1 ) 与 def( k+1 , j )。
此时我们发现了问题,因为戳破了气球 k ,气球数组的相邻关系发生了改变,k-1 与 k+1 原本都与 k 相邻,而 k 戳破后他们两个直接相邻了。而且先戳破 k+1 与先戳破 k-1 得到的结果将完全不同,也就是说两个子问题间发生了依赖。如果先戳破 k-1 ,则 k+1 左边的相邻气球变成了 k-2;反之 k-1 右边相邻的气球变成了 k+2 。
子问题的处理顺序将影响到每个子问题的解,这将使我们的状态转移方程极为复杂和低效,我们应当换一种划分子问题的方式,使每个子问题都是独立的。
那么我们换一种划分方式,既然两个子问题都依赖 k 和两个边界,那么我们划分子问题时,k 与两个边界的气球我们都不戳破,求出 i+1 到 k-1 与 k+1 到 j-1 之间的解。这样两个子问题间的依赖便被消除了,两个边界及气球 k 不被戳破,两个子问题的依赖都不会越过 k 到另一个子问题上,子问题间是相互独立的。
并且在两个子问题解决后,气球序列还剩下 k 与两个边界的气球没有戳破,那么我们用两个子问题的解与戳破 k 与两个边界的最大值即可求出原问题的解。
那么 def( i , j ) 函数的定义则为,不戳破 i 与 j ,仅戳破 i 与 j 之间的气球我们能得到的最大金币数。
如此划分,状态转移方程为: def( i, j ) = def( i , k ) + def( k , j )+nums[ i ][ j ][ k ]
其中 nums[ i ][ j ][ k ] 为戳破气球 k 时我们能得到的金币数,因为def( i , j )表示戳破 i 到 j 之间的气球,自然包括 k 。
上述方程其实还有问题,前面说过,为了保证我们可以完整的搜索解空间,我们需要尝试所有的子问题划分方式,对于上述状态转移方程,也就是 k 的取值。k 的取值应当介于 i+1 与 j-1 之间,我们尝试所有 k 的取值并从中挑选最大值,这才是原问题真正的解。
真正的状态转移方程应该为:def( i, j ) = max { def( i , k ) + def( k , j )+nums[ i ][ j ][ k ] } | i<k<j
这样我们便找到了用子问题的解来表示原问题的解的方法,或者说子问题的划分方式。因为我们要划分子问题,必然不是只划分一次这么简单。而是要把问题一直划分到不能继续划分,也就是划分到问题规模最小的最小子问题,使效率最大化。
因为 k 是介于 i 与 j 之间的,那么当 i 与 j 相邻时我们的问题将不能再继续划分。此时按照我们对问题的定义,“不戳破 i 与 j ,仅戳破 i 与 j 之间的气球”,因为 i 与 j 之间没有气球,我们得到的金币数是 0 。
为了保证问题定义的正确性,我们向上推演一次。def( i , i+2 ) = def( i , i+1 ) + def( i+1 , i+2 ) + nums[i]*nums[ i+1]*nums[i+2]
def( i , i+1 ) , def( i+1 , i+2 ) 都是最小子问题,返回0。即 def( i , i+2 ) = nums[i]*nums[ i+1]*nums[i+2] 。因为问题的定义我们不戳破 i 与 i+2,所以我们只能戳破 i+1,戳破 i+1得到的金币确实是 nums[i]*nums[ i+1]*nums[i+2] 即 def( i , i+2 ) 。
所以说对于我们的状态转移方程 def( i, j ) = max { def( i , k ) + def( k , j )+nums[ i ][ j ][ k ] } | i<k<j ,回归条件 def( i , i+1 ) = 0 是正确的。
状态转移方程与回归条件都找到了,实现起来就很简单了:
class Solution {
public int maxCoins(int[] nums) {
//对空数组进行处理
if(nums==null){
return 0;
}
//由于dp[i][j]并不戳破第i和第j个气球,因此需要添加虚拟边界
int len = nums.length;
int[] num1 = new int[len+2];
//因为题目中说明 nums[-1] = nums[n] = 1,因此可以设置边界为1
//将nums数组的数据填充到num1数组的第1~len位置
System.arraycopy(nums,0,num1,1,len);
//将左边界0、右边界len+1设置为1
num1[0]=1;
num1[len+1]=1;
//创建dp数组
int[][] dp = new int[len+2][len+2];
//调用分治函数
return maxCoins4M(num1,len,0,len+1,dp);
}
/**
*
* @param coin 气球的数组
* @param length 气球的个数
* @param begin 开始的下标(开区间)
* @param end 结束的下标(开区间)
* @param dp 缓存数组
* @return
*/
private int maxCoins4M(int[] coin, int length, int begin, int end, int[][] dp) {
if (begin+1==end){
//此时该区间内没有气球可以被戳破
return 0;
}
if (dp[begin][end]!=0){
//避免重复计算
return dp[begin][end];
}
//维护一个最大值
int max = 0;
for (int i=begin+1;i<end;i++){
int temp = maxCoins4M(coin,length,begin,i,dp)+
maxCoins4M(coin,length,i,end,dp)+coin[begin]*coin[i]*coin[end];
if (temp>max){
max=temp;
}
}
dp[begin][end]=max;
return max;
}
}
此题转载Burst Balloons(leetcode戳气球,困难)从指数级时间复杂度到多项式级时间复杂度的超详细优化思路(回溯到分治到动态规划)