猴子也能看懂的01背包动态规划!超形象详解!小白向!

1. 什么是01背包问题

有一个容量为V的背包,和编号为1…n的n个物品,他们的体积各不相同,价值也各不相同。求问将这些物品放入背包中,如何使背包中的总价值最大。

2. 输入值与输出值

int V 		背包容量 	
int[] c 	储存物品体积的数组,c[i]表示第i件物品的体积。 
int[] v 	储存物品价值的数组,v[i]表示第i件物品的价值。

输出:int maxValue 	在这个背包中任意放入1...n这些物品,能够实现的最大总价值

3. 解决思路

首先,创建一个表,用二维数 f[i][j] 组表示

其中,i表示物品编号,j表示背包容量,f[i][j]的含义为:在考虑1到i个物品可以放入,并且背包容量为j的情况下,最优解(即最大背包中物品的总价值)为f[i][j]。这样理解起来比较抽象,我们将这个表格图像化出来方便理解:在这里插入图片描述
对于这个表格单元f[2][4]来说,它就代表了当背包容量为4,可选物品为1-2号物品时,背包总价值的最大值。 所以,当背包容量为V,可选物品为1到n时,我们的最优解是f[n][V]。为了找到这个值,我们将采用递归思路,从f[i][0]和f[0][j]出发,通过一定的递归规则,遍历整个表格并依次推算,最终找到f[n][V]。那么,最关键的便是找到初始条件f[0][0],以及递归条件。

4. 初始条件

显而易见,当f[i][j]中,可选物品和背包容量任何一个值为0时,也就是说没有可选物品,或者背包无法放入东西时,我们的最优解为0。通过遍历将0写入。这是我们的初始条件
在这里插入图片描述

5. 递归条件

要寻找递归条件,也就是说我们要寻找表格前项和后项之间的关系,即寻找后项f[i][j]和已知前项f[x][y](x<i,y<j)之间的关系,这样,通过递归,我们就可以从初始条件f[0][j]和f[i][0]出发,通过递归关系寻找f[1][j],f[2][j]…直至f[n][j]。那么,此处的递归条件是什么呢?

  1. 对于f[i][j],很显然,如果i的体积超过了背包的容量j,那么i肯定不可以被放入背包,此时f[i][j]=f[i-1][j]

  2. 如果i的体积小于背包容量j,我们可以分为两种情况:
    a. 如果我们不将第i件物品放入背包,此时我们分析可以知道,此时的最优解应该和f[i-1][j]相同
    b. 如果我们将第i件物品放入背包,我们首先要确保背包有足够空间能够装下第i件物品。在这个条件下,我们为了实现价值最大化,需要背包先在未装入第i件物品的时候实现价值最大化,然后再装入第i件物品
    在这里插入图片描述
    其中, f[i][j]=f[i-1][j-c[i]]+v[i] 可以理解为,此时的最大价值=在背包中腾出物品i所需的空间且没有放入i的情况下的最大价值,加上i的价值。
    在这里插入图片描述

那么,在求解f[i][j]时,到底放不放入i呢?显而易见,这取决于上面两种情况哪一种算得的最大价值,所以我们得到最终的递归条件为:
f[i][j]=max{f[i-1][j],f[i-1][j-v[i]]},即等于情况a和情况b相比之下较大的那一个最大价值。
这样,我们就将f[i][j]和已经求得的部分关联起来了。最终我们只要从初始条件出发,就能计算出整张表格。

例子

在一个背包容量为10,可用物品为4个(价值和体积见下表)的背包问题中:
求在表格中f[3][6]的值,即可用物品为1…3,背包容量为6时的最大价值
a. 不放入物品3,那么f[3][6]=f[2][6]
b. 放入物品3,那么f[3][6] = f[2][6-c[3]]+v[3] = f[2][6-4]+8 = f[2][2]+8

在这里插入图片描述
在这里插入图片描述
而最终f[3][6]的取值,只需要取ab中较大的那一个的值就可以了。

5. 最终代码

/**
     * @param V 背包的容积
     * @param c 储存着1...i物品体积的数组
     * @param v 储存着1...i物品价值的数组
     * @return 体积为V,可用物品为1...i时,背包的最大价值
     */
    public static int maxValue(int V, int[] c, int[] v) {
        //初始化所需变量
        int n = c.length;                   //物品数量n
        int f[][] = new int[n + 1][V + 1]; //+1是为了满足表格中,背包体积=0和可用物品为0的情况

        //初始条件,注意循环条件要覆盖全表格
        for (int i = 0; i <= n; i++)
            f[i][0] = 0;
        for (int j = 0; j <= V; j++)
            f[0][j] = 0;

        //递归填充表格,注意在获取物品的体积和价值时,使用c[i-1],v[i-1],因为这里的i是指的物品实际标号,其在c[],v[]中的索引位置要-1;
        //逐行填充
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= V; j++) {
                if (c[i - 1] > j)//如果当前物品i的体积超过了背包容量
                    f[i][j] = f[i - 1][j];
                else
                    f[i][j] = Math.max(f[i - 1][j], f[i - 1][j - c[i - 1]] + v[i - 1]);
            }
        }
        //返回表格中最后的值
        return f[n][V];
    }

-----------测试--------------------------------------------------------------------

对于下表的物品,在背包容量=10的时候
在这里插入图片描述
在这里插入图片描述
输出结果为:
在这里插入图片描述
f[][]表格
在这里插入图片描述

6. 空间优化!!!

我们很容易发现,其实我们并不需要记录整张表格,因为在递归的时候,我们参考的前值总是左侧和上面一排的!不理解?我们来看上面完成后的f[][]表格
在这里插入图片描述

我们前面得到的递归方程为f[i][j] = max{f[i-1][j], f[i-1][j-c[i]]+v[i]}
在max中的两个待选项(即物品i是否放入的两个情况)中,前者是左边一格的数据(蓝色),是本循环上一次得到的数据,后者是上面一排中,相对f[i][j]左侧的数据(橙色),是外循环上一次得到的数据。(黄色为f[i][j])

显然,我们可以通过不断刷新一个一维数组来完成同样的事,如下图:
在这里插入图片描述
如果我们从右往左进行多次遍历,不断刷新这个一维数组,我们就可以大大节省空间。
新的递归条件:f[j]=max{f[j], f[j-c[i]]+v[i]}
我们再次进行分析:

  • 不放入物品i,此时f[j]就等于上一次循环中f[j]的值(上一次循环中,可选物品自然是0…i-1)
  • 放入物品i,此时f[j]等于f[j-c[i]]+v[i](同样,上一次循环可选物品自然是0…i-1)

这样,新的递归条件允许我们使用一维数组完成算法。需要注意的是,我们改变遍历方向的原因,是因为我们需要使用上一次遍历左侧的值,如果我们依旧从左往右遍历,上一次遍历左侧的值将会被覆盖。

7. 优化后代码

import static sun.misc.Version.print;


    /**
     * @param V 背包的容积
     * @param c 储存着1...i物品体积的数组
     * @param v 储存着1...i物品价值的数组
     * @return 体积为V,可用物品为1...i时,背包的最大价值
     */
    public static int maxValue(int V, int[] c, int[] v) {
        //+++++++++++++第一步:初始化所需变量+++++++++++++++++
        int n = c.length;
        //创建动态优化表格f,第一次遍历时可选物品为0, 背包容量为0...V
        int[] f = new int[V + 1];

        //+++++++++++++第二步:在可选物品为0时,无论背包容量多大,最大价值都是0+++++++++++++++++
        for (int i = 0; i <= V; i++) 
            f[i]=0;
        //+++++++++++++第三步:从初始条件开始进行遍历,不断刷新数组以得到最后答案f+++++++++++++++++
        for (int i = 0; i < n; i++) {
            for (int j = V; j >=0 ; j--) {
                if(c[i]>j)
                    f[j]=f[j];//仅为方便理解,其实这里可以省去
                else
                    f[j]=Math.max(f[j],f[j-c[i]]+v[i]);
            }
        }

        //+++++++++++++第四步:遍历完成,输出结果+++++++++++++++++
        return f[V];
    }


恭喜!您学会了吗!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值