赞
踩
给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。
问题分析:面对每一个物品,我们只有选择拿取或者不拿两种选择,不能选择装入某物品的一部分,也不能装入同一物品多次。
0代表这个物品不拿,1代表这个物品拿。
例如:
核心:将问题化为求解子问题,前一个子问题最值问题求解了,如果你找到子问题与当前问题的关系,那么当前问题就解决了,是一个迭代的过程。因此在0-1背包问题中,我们的核心步骤就是用表(通常用一个二维数组)记录子问题的解。
动态规划的步骤:问题结构分析,递推关系建立,自底向上计算,最优方案追踪。
最优子结构性质:问题的最优解由相关子问题最优解组合而成,子问题可以独立求解。
通过枚举列出所有结果,然后进行比较
动态规划求解该问题的重点就在于列出这个表格(通过dp方程式,也就是状态转移方程式),最后求得最优解。
dp方程式(状态转移方程式):
设i为商品的序号,c为容量
if (c<v[ i ])
dp[ i ][ c ] = dp[ i - 1 ][ c ]
else
dp[ i ][ c ] = max (dp[ i-1 ][ c ] , dp[ i-1 ][ c - v[ i ]] + p[ i ])
列表步骤
最优解追踪:
回溯解决方案,倒叙判断是否选择商品,根据选择结果,确定最优子问题。
可以参考一下b站up主的视频(这里感谢一下up主,一键三连!!!)听懂不翻车系列之--背包问题(01背包 完全背包 多重背包 二维费用背包)
背包问题我们日常使用的话0-1和完全背包已经够用了,up主讲的很详细。
下面列出代码编译的过程。
按照上述伪代码流程稍作修改就行。
- public class KnapsackDP{
- public void DP(){
-
- }
- public void OutputDP(){
-
- }
- public static void main(String[] args){
- KnapsackDP Func = new KnapsackDP();
- int[] v={10,3,4,5,4};//背包容量
- int[] p={24,2,9,10,9};//商品价格
- int[][] dp=new int[p.length][13];//二维数组,横轴i表示商品序号,竖轴c表示的剩余容量//此处背包容量暂设为C=13
- Func.DP();
- }
- }
- public class KnapsackDP{
- public void DP(int num,int C,int []vi,int []pi,int[][] dp,int[][] Rec){
- //初始化二维数组dp
- for(int i=0;i<=C;i++){
- dp[0][i]=0;
- }
- for(int i=0;i<=num;i++){
- dp[i][0]=0;
- }
- //开始求解二维数组dp
- for(int i=1;i<=num;i++){
- for(int c=1;c<=C;c++){
- if(c<vi[i]){
- //如果背包容量c小于第i个商品的容量vi,就拿该商品
- dp[i][c]=dp[i-1][c];
- Rec[i][c]=0;
- }else if(dp[i-1][c]>=( dp[i-1][c-vi[i]] + pi[i] )){
- //这个条件和下面的条件属于第二种情况,拿或者不拿商品
- //如果背包中已经拿的商品价值本身就比第i个商品的价值大,那么就不拿
- dp[i][c]=dp[i-1][c];
- Rec[i][c]=0;
- }else if(dp[i-1][c]<( dp[i-1][c-vi[i]] + pi[i] )){
- //如果第i个商品比背包中所有商品的价值都大,就扔掉背包中所有商品,拿这个第i个商品
- /*------具体可以参考一下dp方程的解释,在博客上---------*/
- dp[i][c]=dp[i-1][c-vi[i]]+pi[i];
- Rec[i][c]=1;
- }
- }
- }
- //输出代表价值的二维数组dp和用来回溯的二维数组Rec
- for(int i=0;i<=num;i++) {
- for (int c = 0; c <= C; c++) {
- System.out.print(dp[i][c] + "\t");
- }
- System.out.println();
- }
- System.out.println();
- for(int i=1;i<=num;i++) {
- for (int c = 0; c <= C; c++) {
- System.out.print(Rec[i][c] + " ");
- }
- System.out.println();
- }
- String[] name ={" ","啤酒","汽水","饼干","面包","牛奶"};//这里对商品名称定义一下
- OutputDP(num,C,vi,Rec,name);//这里开始回溯来查找具体拿了哪些商品
- //具体思路就是我们在已经求得最优解的情况下,对最优解进行倒叙回溯。
- }
- public void OutputDP(int num,int C,int []vi,int[][] Rec,String []name){
- int K=C;
- for(int i=num;i>=1;i--){
- if(Rec[i][K]==1){
- System.out.println("选择商品"+name[i]);
- K=K-vi[i];//从最优解开始减背包中商品的容量
- }else{
- System.out.println("不选择商品"+name[i]);
- }
- }
- }
- public static void main(String[] args){
- KnapsackDP Func = new KnapsackDP();
- int[] v={0,10,3,4,5,4};//背包容量
- int[] p={0,24,2,9,10,9};//商品价格
- int[][] dp=new int[p.length][13+1];//二维数组,横轴i表示商品序号,竖轴c表示的剩余容量//此处背包容量暂设为C=13
- int[][] Rec=new int[p.length][13+1];
- Func.DP(5,13,v,p,dp,Rec);//num为商品个数,C为背包容量
- }
- }
出现了一次问题,在定义代表价值的二维数组dp和用来回溯商品的二维数组Rec时,数组下标越界了。一定要注意dp[ i ][ c ]和Rec[ i ][ c ] 中i和c的取值范围,还有对dp和Rec初始化后进行求解时的范围,严格按照背包容量和商品序号的范围进行定义。(这里可以参考我上面图片中手写的表,根据表格的数据对for循环进行定义)
另外,dp和Rec数组的i的取值范围也不一样,dp的i是从0到5一共6个数据,而Rec的i是从1到5一共5个数据,两个的c都是一样的总共13。
最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
这里根据我的理解,动态规划就是在分治法的基础上,解决分治法带来的冗余问题,避免重复计算,因此就有了通过表格来记录已解决的子问题的答案。
详情参考 动态规划_百度百科
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。