背包基础知识

背包基础知识

一、背包分类

面试掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包。

完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。
在这里插入图片描述

二、01背包

N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大
在这里插入图片描述
比如:背包重量最大为4

物品为:

在这里插入图片描述
问背包能背的物品最大价值是多少?

2. 二维dp数组01背包

基本思路

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放
用子问题定义状态:即f[i][j]表示前i件物品恰放入一个容量为j的背包可以获得的最大价值。

则其状态转移方程便是:

f[i][j] = max( f[i-1] [j], f[i-1][j-w[]] + o[i])

这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。

所以有必要将它详细解释一下:“将前i件物品放入容量为j的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。

  • 如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为j的背包中”,价值为f[i-1][j];

  • 如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为j一w[i]的背包中”,此时能获得的最大价值就是f[i-1][j-wi]]再加上通过放入第i件物品获得的价值v[i]

具体求解

依然动规五部曲分析一波。

  1. 确定dp数组以及下标的含义

    对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

    只看这个二维数组的定义,大家一定会有点懵,看下面这个图:i表示物品,j表示背包容量
    在这里插入图片描述

    背包容量为j:表示可以放入容量为0、1、2、3、…、j的物品。

  2. 确定递推公式

    再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

    那么可以有两个方向推出来dp[i][j]:

    由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]

    dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

    所以递归公式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

  3. dp数组如何初始化

    关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。

    首先从dp[i][j]的定义触发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图:
    在这里插入图片描述
    再看其他情况。

    状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

    dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。 代码如下:

    //根据递推公式,需要初始化dp[0][j]的情况
    for (int j = bagV; j >= weight[0]; j--) {
        //编号为 0 的物品放入容量为 j 的背包的最大价值
        dp[0][j] = dp[0][j - weight[0]] + value[0];
    }
    

    注意这个遍历,一定要是倒叙,顺序不行

     例如dp[0][1]是15,到了dp[0][2]=dp[0][2-1]+15;也就是dp[0][2]=30了,那么就是物品0被重复放入了。
    
     所以一定要倒叙遍历,保证物品只被放入一次!这一点对01背包很重要,后面在讲解滚动数组的时候,
     还会用到倒叙遍历来保证物品使用一次!
    

    或者可以这样初始化:

    //放入第一件物品时,当背包容量j大于等于第一件物品的体积时,价值都是value[0]
    for (int j = weight[0]; j <= bagV; j++) {
        dp[0][j] = value[0];
    }
    

    此时dp数组初始化情况如图所示:
    在这里插入图片描述
    dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢?

    dp[i][j]在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,因为0就是最小的了,不会影响取最大价值的结果。

    如果题目给的价值有负数,那么非0下标就要初始化为负无穷了

     例如:一个物品的价值是-2,但对应的位置依然初始化为0,那么取最大值的时候,就会取0而不是-2了,
     所以要初始化为负无穷。
    

    这样才能让dp数组在递归公式的过程中取最大的价值,而不是被初始值覆盖了。

  4. 确定遍历顺序

    那么问题来了,先遍历 物品还是先遍历背包重量呢?

    其实都可以!!但是先遍历物品更好理解。

    for (int i = 1; i < weight.length; i++) {
        for (int j = 1; j <= bagV; j++) {
            if (j < weight[i]) {
                //第i件商品放不进去
                dp[i][j] = dp[i - 1][j];
            } else {
                //选取最优解
                dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
            }
        }
    }
    

    如果出现错误,可以打印出来dp数组进行验证

代码实现:
public class Test {
    public static void main(String[] args) {
        //商品的重量
        int[] weight = {1, 3, 4};
        //商品对应的价值
        int[] value = {15, 20, 30};
        //背包的大小
        int bagV = 4;

        //dp[i][j]表示容量为j时,前i件的最大价值
        int[][] dp = new int[weight.length][bagV + 1];
        //根据递推公式,需要初始化dp[0][j]的情况
        // for (int j = bagV; j >= weight[0]; j--) {
        //     //编号为 0 的物品放入容量为 j 的背包的最大价值
        //     dp[0][j] = dp[0][j - weight[0]] + value[0];
        // }
        //放入第一件物品时,当背包容量j大于等于第一件物品的体积时,价值都是value[0]
        for (int j = weight[0]; j <= bagV; j++) {
            dp[0][j] = value[0];
        }

        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j <= bagV; j++) {
                if (j < weight[i]) {
                    //第i件商品放不进去
                    dp[i][j] = dp[i - 1][j];
                } else {
                    //选取最优解
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
                }
            }
        }
        for (int i = 0; i < weight.length; i++) {
            for (int j = 0; j <= bagV; j++) {
                System.out.print(dp[i][j] + " ");
            }
            System.out.println();
        }
    }
}
找最优解组成

通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些商品组成,故要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式

V(i,j)=V(-1,j)时,说明没有选择第i个商品,则回到V(i-1,j);
V(i,j)=V(i-1,j-w(i))+v(i)时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(-1,j-());
一直遍历到i=0结束为止,所有解的组成都会找到。

比如:

最优解为V(4,8)=10,而V(4,8)!=V(3,8)却有V(4,8)=V(3,8-w(4))+v(4)=V(3,3)+6=4+6=10,所以第4件商品被选中,并且回到V(3,8-
w(4))=V(3,3);
有V(3,3)=V(2,3)=4,所以第3件商品没被选择,回到V(2,3);
而V(2,3)!=V(1,3)却有V(2,3)=V(1,3-w(2))+v(2)=V(1,0)+4=0+4=4,所以第2件商品被选中,并且回到V(1,3-w(2))=V(1,0);
有V(1,0)=V(0,0)=0,所以第1件商品没被选择。

在这里插入图片描述
代码实现:

import java.util.Arrays;

public class Test {
    public static void main(String[] args) {
        //商品的重量
        int[] weight = {1, 3, 4};
        //商品对应的价值
        int[] value = {15, 20, 30};
        //背包的大小
        int bagV = 4;

        //dp[i][j]表示容量为j时,前i件的最大价值
        int[][] dp = new int[weight.length][bagV + 1];
        //记录最优解的情况
        int[] item = new int[weight.length];
        for (int j = weight[0]; j <= bagV; j++) {
            dp[0][j] = value[0];
        }
        //动态规划
        findDp(dp, weight, value, bagV);
        //查找最优解
        findItem(dp, weight, value, weight.length - 1, bagV, item);
        System.out.println(Arrays.toString(item));
    }

    private static void findItem(int[][] dp, int[] weight, int[] value, int i, int j, int[] item) {
        if (i >= 1) {
            //第 i 件商品没有选择
            if (dp[i][j] == dp[i - 1][j]) {
                item[i] = 0;
                findItem(dp, weight, value, i - 1, j, item);
                //    第 i 件商品选择了
            } else if (j - weight[i] >= 0 && dp[i][j] == dp[i][j - weight[i]] + value[i]) {
                item[i] = 1;
                findItem(dp, weight, value, i - 1, j - weight[i], item);
            }
        //    处理编号为0的物品,也就是第一件物品
        } else if (value[0] == dp[0][j]) {
            item[0] = 1;
        }
    }

    private static void findDp(int[][] dp, int[] weight, int[] value, int bagV) {
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j <= bagV; j++) {
                if (j < weight[i]) {
                    //第i件商品放不进去
                    dp[i][j] = dp[i - 1][j];
                } else {
                    //选取最优解
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
                }
            }
        }
    }
}

优化空间复杂度

以上方法的时间和空间复杂度均为O(VN),其中时间复杂度已经不能再优化了,但空间复杂度却可以优化到O(N)。

先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1…N
每次算出来二维数组f[ij]0…]的所有值。
那么,如果只用一个数组f0…],能不能保证第i次循环结束后f[j]中表示的就是我们定义的状态f[i][j]呢?
f[i][j]是由f[i-1][j]和f[i-1][j-w[i]两个子问题递推而来,能否保证在推f[i][j]时(也即在第i次主循环中推f[j]时)能够得到
f[i-1][i]和f[i-1][j-w[i]的值呢?
事实上,这要求在每次主循环中我们以j=V…0的顺序推f[j],这样才能保证推f[j-w[]]保存的是状态f[i-1][j-w[i]]的值。
至于为什么下面有详细解释。代码如下:

for (int i = 1; i <= n; i++)
    for (int j = V; j >= 0; j--)
        f[j] = max(f[j], f[j - w[i]] + v[i]);

其中的f[ij]=mac(f[j],f[j-w[i])-句恰就相当于我们的转移方程f[i][j]=max(f[i-1][j],f[i-1][j-w[i]),因为现在的f[j-w[]]就相当于原来的f{[i-1][j-w[i]]。
如果将V的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][j]由f[i][j-w[i]推知,与本题意不符
但它却是另一个重要的背包问题(完全背包)最简捷的解决方案,故学习只用一维数组解01背包问题是十分必要的。
下面再给出一段代码,注意和上面的代码有什么不同

for (int i = 1; i <= n; i++)
    for (int j = V; j >= w[i]; j--)
        f[j] = max(f[j], f[j - w[i]] + v[i]);

注意这个过程里的处理与前面给出的代码有所不同。前面的示例程序写成j=V…0是为了在程序中体现每个状态都按照方程求解了,避
免不必要的思维复杂度。而这里既然已经抽象成看作黑箱的过程了,就可以加入优化。费用为w[]的物品不会影响状态f0…j-1],这
是显然的。
优化后的背包求放入的最大价值

public class Test {
    public static void main(String[] args) {
        //商品的重量
        int[] weight = {1, 3, 4};
        //商品对应的价值
        int[] value = {15, 20, 30};
        //背包的大小
        int bagV = 4;
        //dp[j]表示容量为j时,前i件的最大价值
        //这里之所以长度都加一,是因为被包容量可以取到4
        //初始dp[0]=0,表示被包容量为0时,可容纳的最大价值为0
        int[] dp = new int[bagV + 1];
        //动态规划
        findDp(dp, weight, value, bagV);
        System.out.println(Arrays.toString(dp));
    }

    //查找动态规划表
    private static void findDp(int[] dp, int[] weight, int[] value, int bagV) {
        for (int i = 0; i < weight.length; i++) {
            for (int j = bagV; j >= weight[i]; j--) {
                //选取最优解
                //每次都借助上一行的dp[j]
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
    }
}

这里存在一个问题就是,可以找到最优解,但是,具体最优解的组成没法确定了

初始化时的细节问题
  • 对于非零下标,因为题目都是正整数,所以初始化为0就可以,因为0就是最小的,不会影响取最大值结果

  • 如果题目有负数,非零下标就要初始化为负无穷

    例如:- 个物品的价值是-2,但对应的位置依然初始化为0,那么取最大值的时候,就会取0而不是-2了 所以要初始化为负无穷。

    参考

    [1] https://blog.csdn.net/qq_37767455/article/details/99086678
    [2] https://blog.csdn.net/yandaoqiushenglarticle/details/84782655/
    [3] https://mp.weixin.qq.com/s/FwliPPmR18_AJO5eiidT6w

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值