0-1背包问题的动态规划实现(java代码)

一,0-1背包问题?

给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高

问题分析:面对每一个物品,我们只有选择拿取或者不拿两种选择,不能选择装入某物品的一部分,也不能装入同一物品多次。

0代表这个物品不拿,1代表这个物品拿。


例如:


 二,动态规划?

核心:将问题化为求解子问题,前一个子问题最值问题求解了,如果你找到子问题与当前问题的关系,那么当前问题就解决了,是一个迭代的过程。因此在0-1背包问题中,我们的核心步骤就是用表(通常用一个二维数组)记录子问题的解。

动态规划的步骤:问题结构分析,递推关系建立,自底向上计算,最优方案追踪。

最优子结构性质:问题的最优解由相关子问题最优解组合而成,子问题可以独立求解。

三,求解方案

1,暴力求解

通过枚举列出所有结果,然后进行比较


2,带备忘进行递归的暴力求解

 3,动态规划

 动态规划求解该问题的重点就在于列出这个表格(通过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主讲的很详细。

4,伪代码

 

5,求解思路

 

 

下面列出代码编译的过程。

按照上述伪代码流程稍作修改就行。


四,动态规划的代码构建(java)

1,代码框架

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();
    }
}

2,完整代码

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为背包容量
    }
}


3,代码的注意点

出现了一次问题,在定义代表价值的二维数组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。

五,动态规划的特点

最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。

无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。

子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。


动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式

这里根据我的理解,动态规划就是在分治法的基础上,解决分治法带来的冗余问题,避免重复计算,因此就有了通过表格来记录已解决的子问题的答案。

详情参考 动态规划_百度百科

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值