01背包问题(背包问题的基础)

01背包问题简介🦈

有N种物品和一个容量为V 的背包。第i种物品最多有1件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
image


01背包问题的特征(重要)❓

每个元素只有选和不选两种选择❗


解决方法(动态规划)🍦

动态规划五部曲🔽

1、确定dp数组以及下标的含义(这点很重要!!!)

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

再后面要一直记得dp数组的意义

image

2、确定递推公式

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

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

  • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
  • 放物品i:由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。如图:

image

当背包容量为0时可以放入的物品数量为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的物品的时候,各个容量的背包所能存放的最大价值。

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

当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。

4、确定遍历顺序

可以看出,有两个遍历的维度:物品与背包容量

这里可以先遍历背包,也可以先遍历容量,具体的遍历顺序要通过递推公式来确定

dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

我们先遍历物品,在遍历背包试试:

1、物品0:

image

2、物品1:
image

3、物品2:

image

dp[i-1][j]=35, dp[i-1][j-weight[i-1]]+values[i-1]=30

所以max=35为最佳值

5、值得注意的点🚁

  1. 关于遍历顺序(个人更喜欢先遍历物品,在遍历背包)

  2. 关于当前层和上一层的关系(数据的获取)

    • 在二维数组dp中:当前层的数据都是由上一层获得(这点要与后面的滚动数组求解01背包区分开)
  3. 关于关于状态转移方程(递归公式)

    dp[i][j] = math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);

image

  • 选择dp[i-1][j-weight[i]]+value[i]表示的是将当前的物品放入背包中,并且当背包容量为j-weight[i]的时候,其价值+当前物品的价值要大于dp[i-1][j]
    • 红色格子中,dp[2-1][4]表示bag中只有物品0,dp[2-1][4-3]+value[2-1]表示放入物品0和物品1,这里需要注意的点是dp[2-1][4-3]的值是来自于红色格子的上一行(遍历物品0时得到的)的蓝色格子也就是说当前层的数据由上一层获得
  • 选择dp[i-1][j]表示的是上一层的容量为j时可以放入的物品(先遍历物品在遍历背包的情况下),即现在的物品我们的背包是放不下或者将现在的遍历到的物品放入背包后价值放而变小
    • 绿色格子中,dp[i-1][j-weight[i-1]]+value[i-1]=dp[3-1][4-4]+value[3-1]表示bag里只有第四个物品,而原来bag中(dp[3-1][4],数据由上一层获得)有物品1和物品2,如果放入第四个物品,背包价值反而变小了,所以背包的物品不变(仍为dp[3-1][4])。

代码实现🌰

    public static void main(string[] args) {
        int[] weight = {1, 3, 4};
        int[] value = {15, 20, 30};
        int bagsize = 4;
        testweightbagproblem(weight, value, bagsize);
    }

    public static void testweightbagproblem(int[] weight, int[] value, int bagsize){
        int wlen = weight.length, value0 = 0;
        //定义dp数组:dp[i][j]表示背包容量为j时,前i个物品能获得的最大价值
        int[][] dp = new int[wlen + 1][bagsize + 1];
        //初始化:背包容量为0时,能获得的价值都为0
        for (int i = 0; i <= wlen; i++){
            dp[i][0] = value0;
        }
        //遍历顺序:先遍历物品,再遍历背包容量
        //值得注意的是我们的物品、背包容量的索引都是从1开始的
        for (int i = 1; i <= wlen; i++){
            for (int j = 1; j <= bagsize; j++){
                if (j < weight[i - 1]){
                    dp[i][j] = dp[i - 1][j];
                }else{
                    dp[i][j] = math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
                }
            }
        }
        //打印dp数组
        for (int i = 0; i <= wlen; i++){
            for (int j = 0; j <= bagsize; j++){
                system.out.print(dp[i][j] + " ");
            }
            system.out.print("\n");
        }
    }

优化:滚动数组解决01背包问题😆

思路转换💡

我们知道,在一维数组实现背包问题的过程中,我们的数据都是从上一层获取的,那么我们是不是可以从当前层获取数据?

答案是可以的,不过这里有一些地方需要注意,比如背包容量的遍历顺序只能逆序并且只能先遍历物品然后逆序遍历背包容量!!!

1、dp数组的含义:dp[j]表示bagSize=j时背包物品的最大价值

2、dp数组初始化:dp[0]=0

3、递推公式的确定:dp[j]=Math.max(dp[j],dp[j-weight[0]]+values[i])

4、遍历顺序:只能先顺序遍历物品,然后逆序遍历背包容量

5、模拟实现(略)

代码实现🧀

class Solution(){
    int 01bag(int[] weight int[] values,int bagSize){
        //索引表示背包的容量,dp[i]表示容量为i时可以存入的最大价值
        int[] dp=new int[bagSize+1];
        //数组的初始化
        dp[0]=0;
        //遍历顺序:先物品,后背包容量
        for(int i=0;i<weight.length;i++){
            for(int j=bagSize;j>=weight[i];j--){
               dp[j]=Math.max(dp[j],dp[j-weight[i]]+values[i]);
            }
        }
        return dp[bagSize];                                    
    }
}

关于只能先遍历物品然后逆序遍历背包的原因🤓

1、我们先考虑将背包容量顺序遍历🐜

//weight={1,3,4},values={15,20,30}
for(int i=0;i<weight.length;i++){
    for(int j=weight[i];i<=bagSize;j++){
        dp[j]=Math.max(dp[j],dp[j-weight[i]+values[i]]);
    }
}

当i=0,

  • bagSize=1时,背包内的物品为物品0,价值为15
  • bagSize=2时,dp[2]=Math.max(dp[2],dp[1]+values[0]),由于dp[1]中已经装入了物品0,我们再次装入物品0就不再满足01背包,而是完全背包的特征,所以该方法不可用

2、我们将两层for循环颠倒并顺序遍历试试⚓

//weight={1,3,4},values={15,20,30}
for(int i=1;i<=bagSize;i++){
    for(int j=0;i<weight.length;j++){
        if(i>=weight[j]){
        	dp[i]=Math.max(dp[i],dp[i-weight[j]+values[j]]);
        }
    }
}
  1. 当i=bagSize=1
    • j=0时,weight[0]=1:dp[1]=[0]+values[0]=15
    • j=1时,weight[1]=3,i<weight[1]
    • 同j=1时
  2. 当i=bagSize=2
    • j=0时,weight[0]=1:dp[2]=dp[1]+values[0]=30,这里再次出现了物品0被添加两次的情况,是完全背包问题的实现方式,不满足01背包

3、两层for循环颠倒,但是bagSize逆序遍历的情况🐼

//weight={1,3,4},values={15,20,30}
for(int i=bagSize;i>0;i++){
	for(int j=0;j<weight.length;j++){
        if(i>=weigth[j]){
         	dp[i]=Math.max(dp[i],dp[i-weight[j]]+values[j]);   
        }
    }
}
  1. 当i=bagSize=4时
    • j=0,weight[0]=1:dp[4]=dp[3]+values[0]=15;
    • j=1,weight[1]=3:dp[4]=dp[1]+values[1]=20;
    • j=2,weight[2]=4:dp[4]=dp[0]+values[2]=30;

遍历完bagSize=4之后我们发现,dp[4]的结果并不是正确的,计算dp[4]的时候,即计算bagSize=4的时候的最大值时,bagSize=3、2和1的dp最大值并不知道,所以我们 没办法通过状态转移 求出dp[4]


posted @ 2022-03-05 11:15  Jyang~  阅读(372)  评论(0)    收藏  举报