动态规划:一维法二维法解决01背包问题 java实现

背包问题

01背包问题

有 n 个不同种物品,它们有各自的体积和价值,现有给定一个固定容量的背包,如何让背包里装入的物品具有最大的价值总和?

如下即:eg:物品个数为 4,背包总容量为 8

i(物品编号)1234
w(体积)2345
v(价值)3456
二维法
定义变量

v数组:表示每一个物品的价值 w 数组:表示每一个物品的所占用的空间

maxWeight :表示背包最大容量 nums 表示物品的最大个数

定义 dp(i,j) 二维数组表示:当前背包容量为 j 时,前 i 个物品所能组成的最大价值(其中 j 为动态分配的背包,容量是不断变化的。1<= j <=maxWeight、1<= i <=nums,j 仅仅作为能不能装物品的界限)。

递归关系式

面对当前商品 i 有两种可能性:

  • 包的容量比该商品体积小,装不下,此时的价值与前 i-1 个的价值是一样的,即dp(i,j)=dp(i-1,j)
  • 还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即dp(i,j)=max{dp(i-1,j),dp(i-1,j-w(i))+v(i)}

其中dp(i,j)=dp(i-1,j)表示第 i 个物品不装,dp(i-1,j-w(i))+v(i))表示装了第 i 个物品,背包容量减少了 w(i),价值增加了 v(i)。

如何理解dp(i-1,j-w(i))

当第i个物品被装进背包,肯定占用了 w(i) 的空间,此时背包里面还有 j-w(i) 的空间了,那么在 j-w(i) 的空间里面,还有前 i-1 个物品可以被装进来,则前 i-1 个物品在容量为 j-w(i) 的背包中所能组成的最大价值是多少呢?很明显可以用之前定义的 dp(i,j) 二维数组表示——dp(i-1,j-w(i))

填表

通过填写表即 dp(i,j) 二维数组(当前最大价值表),把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接从二维数组中提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。

eg:物品个数为4,背包总容量为8

i(物品编号)1234
w(体积)2345
v(价值)3456

定义表空间

初始化边界条件: v(0,j)=v(i,0)=0 即二维数组的第一行和第一列,在给二维数组赋初值的时候就已经做了(这里为了直观的显示就没有将画出来)i:物品编号 j:当前背包容量(动态背包)

i/j12345678
1
2
3
4

填表

从第一行开始填表,当前行利用前一行的最优策略

  • 如,i=1,j=1,w(1)=2,v(1)=3,有j<w(1),故V(1,1)=V(1-1,1)=0;
  • 又如i=1,j=2,w(1)=2,v(1)=3,有j=w(1),故V(1,2)=max{ V(1-1,2),V(1-1,2-w(1))+v(1) }=max{0,0+3}=3;
  • 如此下去,填到最后一个,i=4,j=8,w(4)=5,v(4)=6,有j>w(4),故V(4,8)=max{ V(4-1,8),V(4-1,8-w(4))+v(4) }=max{9,4+6}=10
i/j12345678
103333333
203447777
303457899
4034578910

一行一行的填完:从第一行到最后一行填完得出最大价值v(4,8)=10

代码实现
 /**
     * 基础版的01背包(二维法)
     * @param n 物品个数
     * @param maxWight 背包总容量
     * @param wights 每个物品的体积
     * @param values 每个物品的价值
     */
     public int[][] bag01(int n,int maxWight,int[]wights,int[]values){
        int[][] dp=new int[n+1][maxWight+1];
        for (int i = 1; i <=n ; i++) {
            int w=wights[i-1];
            int v=values[i-1];
            for (int j = 1; j <=maxWight; j++) {
                if (j>=w){
                    dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w]+v);
                }else {
                    dp[i][j]=dp[i-1][j];
                }
            }
        }
        return dp;
    }
一维法
分析二维表

我们会发现二维表有很多重复的,而且我们只用到最后一层的数据,记为i层的数据,要计算第 i 层的数据必须要用到前 i-1 层的数据,只用到这两层,并不会用到其他层的数据前一层作为基础,进行更新从而变成下一层。一层一层的深入直到最后一层,也就是我们用到的最后一层,那这样就可以用一个一维数组了。

公式对比

直接先从公式对比吧,不然真的不好理解这个公式。

  • 二维法:dp(i,j)=Max(dp(i-1,j),dp(i-1,j-w(i))+v(i))
  • 一维法:dp(j)=Max(dp(j),dp(j-w(i))+v(i))

二维法:直观的将第 i-1 层显示出来,表示前 i-1 个物品在容量为 j-w(i) 的背包中所能组成的最大价值。

一维法:dp(j-w(i))已经隐含了:前 i-1 个物品在容量为 j-w(i) 的背包中所能组成的最大价值。因为随着背包容量的减少w(i),说明第i个物品已经装了dp (j-w (i) )的值在前一层已经求出(前提j是逆序的,下面有详细讲解)

for (int j =maxWeight ; j >=1 ; j--) {
                if (j>=w){
                    dp[j]=Math.max(dp[j],dp[j-w]+v);
                }
            }
逆序问题

新问题出来了———— 你怎么能确定 dp(j-w(i)) 一定是:前 i-1 个物品在容量为 j-w(i) 的背包中所组成的最大价值? 为什么要让 j 逆序的去遍历呢 ?

假设使用正序,则在第 i 层进行更新的过程中, j 从小到大依次递增,当进行到dp(j)=Max(dp(j),dp(j-w(i))+v(i))时,特别是计算其中的dp(j-w(i))必须要用到前 i-1 层的数据可是我们在第 i 层中在对 j 进行递增( j 正序递增)的过程中,是不是已经将 dp(j-w(i)) 进行更新过了?因为在 j 的递增中会有之前的j小于现在的j,现在的j-w(i)可能正好等于之前的j,那么此时 dp(j-w(i)) 的数据就不是前 i-1 层的数据了,而是在第 i 层被我们更新的数据! (理解这一点很重要)

因此,让 j 是逆序的,即动态背包是从大到小的。保证了 j-w(i) 的值只能是前 i-1 层的数据,从而让 dp(j-w(i)) 真正的表示为:在背包容量为 j-w(i) 时,前 i-1 个物品所能组成的最大价值

代码实现
 /**
     * 进阶版01背包(一维法)
     * @param n
     * @param maxWeight
     * @param values
     * @param weight
     * @return
     */
    static int[] bag02(int n,int maxWeight,int[]values,int[]weight){
        int[] dp=new int[maxWeight+1];
        for (int i = 1; i <=n ; i++) {
            int w=weight[i-1];
            int v=values[i-1];
            for (int j =maxWeight ; j >=1 ; j--) {
                if (j>=w){
                    dp[j]=Math.max(dp[j],dp[j-w]+v);
                }
            }
        }
        return dp;
    }

输出结果:[0, 0, 3, 4, 5, 7, 8, 9, 10],恰好为最后一层的结果(第一个元素为边界条件,忽略不计)

背包问题最优解回溯

原理

前面的求解只是求出了背包问题的最优价值,可是并不知道是哪一个物品被装了进来,这该如何是好?

通过填表我们发现

  • V(i,j)=V(i-1,j)时,说明没有选择第i 个商品,则回到V(i-1,j);
  • V(i,j)=V(i-1,j-w(i))+v(i)时,说明装了第i个商品,该商品i是最优解组成的一部分,记录下来。随后我们得回到装该商品之前,即回到V(i-1,j-w(i));
  • 一直遍历到i=1结束为止,所有解的组成都会找到。
代码实现
 /**
     * 寻找最优解的物品构成
     * @param i 为物品个数
     * @param j 为背包的总容量
     * 其实只需传入 i,j即可,也可以将int[]wights,int[][] dp,int[]item 定义为成员变量
     */
    static void findWhat(int i ,int j,int[]wights,int[][] dp,int[]item){
        if (i>0) {
            int w = wights[i - 1];
            if (dp[i][j] == dp[i - 1][j]) {
                item[i - 1] = 0;
                findWhat(i - 1, j, wights, dp, item);
            } else  {
                item[i - 1] = 1;
                findWhat(i - 1, j - w, wights, dp, item);
            }
        }
    }

打印item数组得:[0, 1, 0, 1]即为第二件和第四件物品被装入背包。


完整代码

import java.util.Arrays;

public class bag01 {
    public static void main(String[] args) {
        int[] wights={2,3,4,5};//每个物品的体积
        int[] values={3,4,5,6};//每个物品的价值
        int[][] dp = bag01(4, 8, wights, values);
        //打印dp数组
        for (int i = 1; i <=4 ; i++) {
            for (int j = 1; j <=8 ; j++) {
                System.out.print(dp[i][j]+" ");
            }
            System.out.println();
        }
        int[] item=new int[4];
        //寻找最优价值的物品组成
        findWhat(4, 8, wights, dp, item);
        //打印item数组
        System.out.println(Arrays.toString(item));

        //一维法求背包问题
        int[] dp2= bag02(4, 8, values, wights);
        System.out.println(Arrays.toString(dp2));
    }
    /**
     * 基础版的01背包(二维法)
     * @param n 物品个数
     * @param maxWight 背包总容量
     * @param wights 每个物品的体积
     * @param values 每个物品的价值
     */
    static int[][] bag01(int n,int maxWight,int[]wights,int[]values){
        int[][] dp=new int[n+1][maxWight+1];
        for (int i = 1; i <=n ; i++) {
            int w=wights[i-1];
            int v=values[i-1];
            for (int j = 1; j <=maxWight; j++) {
                if (j>=w){
                    dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w]+v);
                }else {
                    dp[i][j]=dp[i-1][j];
                }
            }
        }
        return dp;
    }
    /**
     * 进阶版01背包(一维法)
     * @param n
     * @param maxWeight
     * @param values
     * @param weight
     * @return
     */
    static int[] bag02(int n,int maxWeight,int[]values,int[]weight){
        int[] dp=new int[maxWeight+1];
        for (int i = 1; i <=n ; i++) {
            int w=weight[i-1];
            int v=values[i-1];
            for (int j =maxWeight ; j >=1 ; j--) {
                if (j>=w){
                    dp[j]=Math.max(dp[j],dp[j-w]+v);
                }
            }
        }
        return dp;
    }

    /**
     * 寻找最优解的物品构成
     * @param i 为物品个数
     * @param j 为背包的总容量
     * 其实只需传入 i,j即可
     */
    static void findWhat(int i ,int j,int[]wights,int[][] dp,int[]item){
        if (i>0) {
            int w = wights[i - 1];
            if (dp[i][j] == dp[i - 1][j]) {
                item[i - 1] = 0;
                findWhat(i - 1, j, wights, dp, item);
            } else  {
                item[i - 1] = 1;
                findWhat(i - 1, j - w, wights, dp, item);
            }
        }
    }


}

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值