戳气球
回溯
第一眼看到题目肯定还是先把暴力回溯解写出来看看到底是个什么名堂。很明显,按照暴力解法,就是一个类似于求子集的题目,中间增加了一些步骤也就是计算当前硬币数量。具体步骤如下:
- 遍历每一个气球,表示要戳破他,然后计算当前的硬币数量。
- 遍历后去最大的值为计算结果。
其中的trick:因为硬币数量大于1,因此可以把戳破的气球设置为-1。而计算当前硬币结果的代码如下(注意,是分别求左右边界,而不是同时去求!):
private int computeCurCoin(int[] nums, int center){
int[] ans ={1,nums[center],1};
int left = center-1;
int right = center+1;
while (left>=0){
if(nums[left] == -1){
left--;
}else {
ans[0] = nums[left];
break;
}
}
while ( right <nums.length){
if(nums[right] == -1){
right++;
}else {
ans[2] = nums[right];
break;
}
}
// while (left>=0 &&right <nums.length){
// if(nums[left]!=-1 &&nums[right]!=-1){
// ans[0] = nums[left];
// ans[2] = nums[right];
// break;
// }
//
// if(nums[right] == -1){
// right++;
// }
// }
return ans[0]*ans[1]*ans[2];
}
然后就可以通过回溯的方法求得结果,暴力回溯代码如下:
public int maxCoins(int[] nums) {
return process(nums,0);
}
private int process(int[] nums, int index) {
if (nums.length == 0 || index == nums.length) {
return 0;
}
int ans = 0;
for(int i=0;i<nums.length;i++){
if(nums[i] == -1){
continue;
}
int tmp = nums[i];
int curCoin = computeCurCoin(nums, i);
nums[i] = -1;
ans = Math.max(process(nums,index+1)+curCoin,ans);
nums[i] = tmp;
}
return ans;
}
记忆化递归
假设左边界为l右边界为r,戳破的位置为k,则当戳破以后,分为两个子问题:[l,k]和[k,j]。但是虽然还是求这两个范围的重复子问题,但是很明显这个不是独立的重叠子问题。因为还可能存在[k-2,k-1,k+1]这种情况。
因此,需要逆向思维,我们选择一个气球,假设他为最后一个,并点爆他。这个时候就可以把左边问题和右边问题独立化了!以[3,1,5]为例,先拿出来一个气球作为最后一个气球,然后点爆他。这样就能够将这个气球的左右两个子问题独立化。换个方式来说,我们选择1这个气球,然后优先吧3和5点爆之后,再最后点爆1这个气球。
=>定义状态转移方程。
dp[i][j]表示从i到j闭区间内能够获取最大硬币的数量。
dp[i][j] = dp[i][k-1]+dp[k+1][j] + nums[i-1]*nums[k]*nums[j+1]
这种方法仍然是O(n!),只是把重复子问题写在内存中,保证每种子问题只计算一遍。
代码如下:
private int process_memo(int[] nums, int begin, int end, Integer[][] memo){
if(memo[begin][end] !=null){
return memo[begin][end];
}
int max = 0;
for(int k=begin+1;k<end;k++){
max = Math.max(max,
process_memo(nums,begin,k,memo) + process_memo(nums,k,end,memo) + nums[begin] * nums[k] * nums[end]);
}
return memo[begin][end] = max;
}
//
public int maxCoins(int[] nums) {
int[] input = new int[nums.length+2];
input[0] = 1;
input[input.length-1] = 1;
for(int i=0;i<nums.length;i++){
input[i+1] = nums[i];
}
// 从0开始 到input.length-1结束。
return process_memo(input,0,input.length-1,new Integer[input.length][input.length])
}
动态规划
自底向上,所有明确计算粒度:从i到j,第一次长度是1,第二次长度为2,直到长度为n。
- 第一层循环定义长度len from 1 to n
- 第二层循环定义遍历的范围1 to n-len-1此时定义j的位置 j = i + len - 1
- 第三层在当前情况下,选取k个气球,当做是最后一次点爆,一共有k种情况,(i<=k<=j)选取最大值。
返回dp[1][n]
代码如下:
public int maxCoins(int[] nums) {
int n = nums.length;
int[][] rec = new int[n + 2][n + 2];
int[] val = new int[n + 2];
val[0] = val[n + 1] = 1;
for (int i = 1; i <= n; i++) {
val[i] = nums[i - 1];
}
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 2; j <= n + 1; j++) {
for (int k = i + 1; k < j; k++) {
int sum = val[i] * val[k] * val[j];
sum += rec[i][k] + rec[k][j];
rec[i][j] = Math.max(rec[i][j], sum);
}
}
}
return rec[0][n + 1];
}