6.2 休息日 背包问题总结

就目前所遇到的01背包与完全背包作总结。

01背包

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weighti,得到的价值是valuei每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

二维dp数组01背包

动规五部曲

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

dpij 表示从下标为0-i的物品里任意取,放进容量为j的背包,价值总和最大是多少

2.确定递推公式

  • 不放物品i:由dpi - 1j推出,即背包容量为j,里面不放物品i的最大价值,此时dpij就是dpi - 1j。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品i:由dpi - 1j - weight\[i]推出,dpi - 1j - weight\[i] 为背包容量为j - weighti的时候不放物品i的最大价值,那么dpi - 1j - weight\[i] + valuei (物品i的价值),就是背包放物品i得到的最大价值

所以递归公式: dpij = max(dpi - 1j, dpi - 1j - weight\[i] + valuei);

3.dp数组如何初始化

首先从dpij的定义出发,如果背包容量j为0的话,即dpi0,无论是选取哪些物品,背包价值总和一定为0。

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

dp0j,即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

那么很明显当j < weight0的时候,dp0j 应该是 0,因为背包容量比编号0的物品重量还小。

当j >= weight0时,dp0j 应该是value0,因为背包容量放足够放编号0物品。

其实从递归公式: dpij = max(dpi - 1j, dpi - 1j - weight\[i] + valuei); 可以看出dpij 是由左上方数值和左方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。

但只不过一开始就统一把dp数组统一初始为0,更方便一些。

4.确定遍历顺序

先遍历物品与先遍历背包均可。虽然两个for循环遍历的次序不同,但是dpij所需要的数据就是左上角(包括左与正上),根本不影响dpij公式的推导!

5.举例推导/打印dp数组

一维数组(滚动数组)

滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。

动规五部曲

1.确定dp数组的定义

在一维dp数组中,dpj表示:容量为j的背包,所背的物品价值可以最大为dpj

2.一维dp数组的递推公式

dpj为 容量为j的背包所背的最大价值,那么如何推导dpj呢?

dpj可以通过dpj - weight\[i]推导出来,dpj - weight\[i]表示容量为j - weighti的背包所背的最大价值。

dpj - weight\[i] + valuei 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dpj

此时dpj有两个选择,一个是取自己dpj 相当于 二维dp数组中的dpi-1j,即不放物品i,一个是取dpj - weight\[i] + valuei,即放物品i,指定是取最大的,毕竟是求最大价值。

3.一维dp数组如何初始化

dpj表示:容量为j的背包,所背的物品价值可以最大为dpj,那么dp0就应该是0,因为背包容量为0所背的物品的最大价值就是0。

那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?

看一下递归公式:dpj = max(dpj, dpj - weight\[i] + valuei);

dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。

4.一维dp数组遍历顺序

与二维dp的写法,遍历背包的顺序是不一样的!

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

那么问题又来了,为什么二维dp数组遍历的时候不用倒序呢?

因为对于二维dp,dpij都是通过上一层即dpi - 1j计算而来,本层的dpij并不会被覆盖!

再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

不可以!

因为一维dp的写法,背包容量一定是要倒序遍历,如果遍历背包容量放在上一层,那么每个dpj就只会放入一个物品,即:背包里只放入了一个物品。

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖

5.举例推导dp数组

完全背包

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weighti,得到的价值是valuei每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

二维数组

动规五部曲

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

dpij 表示从下标为0-i的物品里任意取任意件,放进容量为j的背包,价值总和最大是多少

2.确定递推公式

所以递归公式:dpij = max(dpi-1j, dpi j - weight\[i] + valuei); //同层取n件

3.dp数组如何初始化

首先从dpij的定义出发,如果背包容量j为0的话,即dpi0,无论是选取哪些物品,背包价值总和一定为0。

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

dp0j,即:i为0,存放任意件编号0的物品的时候,各个容量的背包所能存放的最大价值。

那么很明显当j < weight0的时候,dp0j 应该是 0,因为背包容量比编号0的物品重量还小。

当j >= weight0时,dp0j 应该是dp0j = dp0j-weight\[0]+value0; //多次选取,因为背包容量放足够放多件编号0物品。

其实从递归公式: **dpij = max(dpi-1j, dpi j - weight\[i] + valuei);**可以看出dpij 是由左上方数值和左方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。

但只不过一开始就统一把dp数组统一初始为0,更方便一些。

4.确定遍历顺序 在代码中使用了先物品后背包

先遍历物品与先遍历背包均可。虽然两个for循环遍历的次序不同,但是dpij所需要的数据就是左上角(包括左与正上),根本不影响dpij公式的推导!

因为物品有无限个所以内层循环可以正序遍历重复放入。

再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

可以!

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!因为两个for循环都是正序遍历,物品有无数个,先遍历物品与先遍历背包得到的结果是相同的,所以遍历顺序可以改变。因为dpj 是根据 下标j之前(左边)所对应的dpj计算出来的。 只要保证下标j之前的dpj都是经过计算的就可以了。

5.举例推导/打印dp数组

java 复制代码
package day62;


import java.lang.*;

public class day62{
    public static void main (String[] args) {
        int n = 4;
        int w = 5;
        int[][] dp = new int[n][w+1];
        int [] weight = new int []{1,2,3,4};
        int [] value = new int []{2,4,5,6};


        for (int j = weight[0]; j <= w; j++) {
            dp[0][j] = dp[0][j-weight[0]]+value[0];   //多次选取
        }

        for(int i = 1 ; i< n ; i++){
            for(int j = 1; j <= w; j++){
                if (j - weight[i] >= 0) dp[i][j] = Math.max(dp[i-1][j], dp[i][j-weight[i]] + value[i]);
                else   dp[i][j] = dp[i-1][j];
            }
        }

        // 打印dp数组
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= w; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }
        System.out.println(dp[n-1][w]);
    }
}

一维数组

动规五部曲

1.确定dp数组的定义

在一维dp数组中,dpj表示:容量为j的背包,所背的物品价值可以最大为dpj

2.一维dp数组的递推公式

dpj为 容量为j的背包所背的最大价值,那么如何推导dpj呢?

dpj可以通过dpj - weight\[i]推导出来,dpj - weight\[i]表示容量为j - weighti的背包所背的最大价值。

dpj - weight\[i] + valuei 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dpj

此时dpj有两个选择,一个是取自己dpj 相当于 二维dp数组中的dpi-1j,即不放物品i,一个是取dpj - weight\[i] + valuei,即放物品i,指定是取最大的,毕竟是求最大价值,

3.一维dp数组如何初始化

dpj表示:容量为j的背包,所背的物品价值可以最大为dpj,那么dp0就应该是0,因为背包容量为0所背的物品的最大价值就是0。

dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。

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

4.一维dp数组遍历顺序

因为物品有无限个所以内层循环可以正序遍历重复放入。

再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

可以!

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!因为两个for循环都是正序遍历,物品有无数个,先遍历物品与先遍历背包得到的结果是相同的,所以遍历顺序可以改变。因为dpj 是根据 下标j之前(左边)所对应的dpj计算出来的。 只要保证下标j之前的dpj都是经过计算的就可以了。

5.举例推导/打印dp数组

相关推荐
大鱼>6 分钟前
AI+货物追踪:跨境电商包裹追踪系统
人工智能·深度学习·算法·机器学习
大鱼>12 分钟前
AI+货物追踪:全链路货物可视化追踪系统
人工智能·深度学习·算法·机器学习
大江东去浪淘尽千古风流人物17 分钟前
【SLAM】slam高动态位姿估计全链路拆解
算法·面试·职场和发展·视觉里程计·slam·vio·大疆
想你依然心痛20 分钟前
量子计算对嵌入式密码学的挑战与后量子算法准备
算法·密码学·量子计算
Jerry4 小时前
LeetCode 28. 找出字符串中第一个匹配项的下标
算法
Jerry6 小时前
LeetCode 459. 重复的子字符串
算法
海石9 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石9 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
Jerry11 小时前
LeetCode 151. 反转字符串中的单词
算法
a11177613 小时前
LM 算法迭代过程动画演示(SLAM)
算法