11.动态规划:戳气球
戳气球(LeetCode 312题 难度:困难)
题目中说 nums[0]=nums[n]=1;
动态规划思路:
int n=nums.length;
int point[]=new int[n+2];
int m=point.length;
point[0]=point[n+1]=1;
for (int i = 0; i < n; i++) {
point[i+1]=nums[i];
}
现在气球的索引变成了从1到n,nums[0]和nums[n+1]看做两个虚拟气球
那么可以改变问题:在一排气球point中,请戳破0到n+1之间所有气球(不包括0和n+1),使得最终只剩下0和气球n+1两个气球,最多能够得到多少分?
dp数组定义:
dp[i][j]=x表示,戳破气球i和气球j之间(开区间,不包括i和j)的所有气球,
可以获得最高获得的分数为x,简单来说就是 i j 是开区间
根据这个定义,题目中要求的结果 就是 dp[0][n+1]或者dp[0][m-1]
根据以往的经验,可以知道,斜着遍历
base case
dp[i][i]=0;开区间没有气球可以戳破
//所有的数字都已经初始化为0
int dp[][]=new int[m][m]; //m就是n+2
其实气球i和气球j之间的所有气球都有可能是被戳破的那一个,不放假设为k,回顾一下动态规划的套路,这里其实已经找到了状态和选择:i和j就是两个状态,最后戳破的那个气球k就是选择
不是要求戳破气球k嘛,那要把开区间(i,k)的气球都戳破,再把开区间(k,j)的气球也戳破;最后剩下气球k,相邻的气球就是气球i和气球j,这时候戳破k的话得到的分数 point[i] x point[j] x point [k]
那么戳破开区间(i,j)和开区间(k,j)的气球最多可以得到的分数是多少呢?那就是dp[i][k]+dp[k][i]+ point[i] x point[j] x point [k]
那么对于一组给定的i和j,只要穷举 i<k<j 的所有气球k,然后选择得分最高的给dp[i][j]就OK了。
//在i和j这个区间中,最后戳破哪个气球收益最大就给dp[i];
for (int k = i+1; k <j ; k++) {
//择优选择
dp[i][j]=Math.max(dp[i][j],dp[i][k]+dp[k][j]+point[i]*point[k]*point[j]);
}
- 对于k的穷举仅仅是在做选择,但是应该如何穷举 状态 i 和 j 呢?
for (int i =....) {
for (int j =....) {
//择优选择
for (int k = i+1; k <j ; k++) {
dp[i][j]=Math.max(dp[i][j],dp[i][k]+dp[k][j]+point[i]*point[k]*point[j]);
}
}
}
写出代码
关于「状态」的穷举,最重要的一点就是:状态转移所依赖的状态必须被提前计算出来。
拿这道题举例,dp[i][j]
所依赖的状态是dp[i][k]
和dp[k][j]
,那么我们必须保证:在计算dp[i][j]
时,dp[i][k]
和dp[k][j]
已经被计算出来了(其中i < k < j
)。
那么应该如何安排i
和j
的遍历顺序,来提供上述的保证呢?我们前文 动态规划答疑篇 写过处理这种问题的一个鸡贼技巧:根据 base case 和最终状态进行推导。
PS:最终状态就是指题目要求的结果,对于这道题目也就是dp[0][n+1]
。
我们先把 base case 和最终的状态在 DP table 上画出来:
对于任一dp[i][j]
,我们希望所有dp[i][k]
和dp[k][j]
已经被计算,画在图上就是这种情况:
那么,为了达到这个要求,可以有两种遍历方法,要么斜着遍历,要么从下到上从左到右遍历:
public int maxCoins(int[] nums) {
int n=nums.length;
int point[]=new int[n+2];
int m=point.length;
point[0]=point[n+1]=1;
for (int i = 0; i < n; i++) {
point[i+1]=nums[i];
}
int dp[][]=new int[m][m];
for (int i = m-2; i >=0 ; i--) {
for (int j = i+1; j < m ; j++) {
//择优录取
for (int k = i+1; k <j ; k++) {
dp[i][j]=Math.max(dp[i][j],dp[i][k]+dp[k][j]+point[i]*point[k]*point[j]);
}
}
}
return dp[0][m-1];
}
总结
至此,这道题目就完全解决了,十分巧妙,但也不是那么难,对吧?
关键在于dp
数组的定义,需要避免子问题互相影响,所以我们反向思考,将dp[i][j]
的定义设为开区间,考虑最后戳破的气球是哪一个,以此构建了状态转移方程。
对于如何穷举「状态」,我们使用了小技巧,通过 base case 和最终状态推导出i,j
的遍历方向,保证正确的状态转移。