开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
背包问题:0-1背包,完全背包,多重背包,混合三种背包,二维费用背包等等;
0-1背包:有n种物品,每种物品只有一个
完全背包:有n种物品,每种物品有无限个
多重背包:有n种物品,每种物品的个数各不相同
主要体现在物品数量的不同
本文先从最简单的0-1背包问题开始研究##
问题描述:有N件物品和一个可以容纳重量为W的背包,第i件物品的重量是weight[i],价值是value[i],求解应在背包中放入哪些物品使得背包的总价值最大,并且需要满足总重量不超过背包的最大重量。
首先,如果不考虑动态规划,最简单的想法就是:
每种物品都有两种状态,放入背包or不放入背包,所以只需要遍历所有的情况,选择价值最大的那种便好,此时算法的复杂度为O(n2)。
下面,我们来一起看动态规划:
以一个简单的例子学习:假设此时背包能够容纳最大的重量为4
| 物品 | 重量 | 价值 |
|---|---|---|
| 0 | 1 | 15 |
| 1 | 3 | 20 |
| 2 | 4 | 30 |
1.二维数组
(1)定义二维数组dp[i][j],表示的含义为,[0,i]之间的物品任取,放进容量为j的背包里面,最大价值为dp[i][j]
(2)递推公式为:dp[i][j]=max(dp[i-1][j],dp[i-1][j-weght[i]]+value[i])
解释:dp[i-1][j]表示,不放物品i此时的价值,因为此时在[0,i-1]之间取物品,放入容量为j的背包; dp[i-1][j-weight[i]]+value[i]表示放物品i此时的价值,因为dp[i-1][j-weight[i]]表示在[0,i-1]之间任取物品,放入j-weight[i]的背包中,在加上value[i]即i的价值,则表示在[0,i-1+1]中取,放入j-weight[i]+weight[i]的背包中;;
(3)对于dp数组的初始化:具体情况具体分析,不能都初始化为0or1
观察递推公式,dp[i][j],由对应的上一层i-1层和左上角的某个数推出来(左上角是因为i-1<I,j-weight[i]<j),所以初始化第一列和第一行是有必要的,需要思考,其他元素的初始化,需要自行赋予初值,但是不能影响正确结果,比如随便dp[1][3]=100,就会影响结果,我们默认赋予0
| i | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 0 | 0 | 15 | 15 | 15 | 15 |
| 1 | 0 | ||||
| 2 | 0 |
如上表所示,这部分数赋予初值需要考虑,第一列表示的含义为,背包容量为0,此时当然不能放任何物品,价值为0,第一行的含义为,在[0,0]之间的物品任取,就是物品0,在背包容量为0,1,2,3,4,分别能放的最大价值。未填的数据,直接赋予0即可。
(4)遍历顺序:
两层for循环,一个是遍历物品,一个是遍历背包容量
对于二维数组0-1背包问题,两层for循环遍历顺序可以颠倒(如果是一维数组就不可以颠倒,下面会叙述原因)
若第一层循环遍历物品,后一层循环遍历背包容量,那就是先第一行计算行,再计算第二行,不影响结果;
若第一层遍历背包容量,后一层循环遍历物品,那就是先计算第一列,再计算第二列,不影响结果。
2. 一维数组:滚动数组
此时就将相当于把一个二维矩阵压缩为一行,为什么可以这样呢,我们看会二维数组递推公式,dp[i][j]与dp[i-1][j]有关就是其只与上一行有关,dp[i][j]与dp[i-1][j-weight[i]],也是其只与上一行有关,所以,我们在计算第i时,是不是只需要保留第i-1行的值就可以,所以就可以变为一维数组。
(1)明确数组含义:dp[j],容量为j背包所能装的最大价值为dp[j];
(2)递推公式:
dp[j]=max(dp[j],dp[j-wieght[i]]+value[i])
dp[j]表示不放第i个物品,dp[j-wieght[i]]+value[i]表示放第i个物品
(3)初始化,dp[0]=0,非零下标的初始化,非负数里面的最小值,统一初始化为0;
(4)遍历顺序:
for(i=0;i<物品数量;i++)#遍历物品
for(j=bagweight;j>=weight[i];j--)#遍历背包
递推公式,倒叙遍历,保证每个物品只被添加一次
接下来,我们以最上面的例子,回答几个问题
A:为什么都初始化为0?
B:为什么遍历顺序不可以颠倒?如果颠倒结果会怎么样?
C:为什么是倒叙遍历?
| dp[0] | dp[1] | dp[2] | dp[3] | dp[4] |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
按照遍历顺序:
Step1:
先i=0,j=4,weight[0]=1,value[0]=15,由于j>=1,所以,dp[4]=max(dp[4],dp[4-1]+15)=15
J=3,j>=1,dp[3]=max(dp[3],dp[3-1]+15)=15;dp[2]=15,dp[1]=15,dp[0]=0;
Step2:
i=1,j=4,weight[1]=3,value[1]=20,所以,dp[4]=max(dp[4],dp[4-3]+20)=35,dp[3]=20,此时j=2不满足j>3,所以无法进入递推公式
之后想法一样样的,就不唠了
A:初始化为0,是因为由上述的计算可以看出,再递推公式中,只要不赋予很大的初值,影响结果就可,所以为了保险起见,赋予初值为0;
B:如果遍历顺序颠倒,那就是
for(j=bagweight;j>=weight[i];j--)#遍历背包
for(i=0;i<物品数量;i++)#遍历物品
递推公式,来计算一下,
当j=4时
dp[4]=max(dp[4],dp[4-1]+15)=15
dp[4]=max(dp[4],dp[4-3]+20)=20,
dp[4]=max(dp[4],dp[4-4]+30)=30;
当j=3时,3<=weight[2]=4,所以根本不会改变值,进入循环
当j=2,j=1时,情况一样
也可以逻辑分析一下,二维数组第i行由第i-1行决定,所以就要先计算第i-1行,就要第一层循环是物品,第二层循环是背包
C:for(j=bagweight;j>=weight[i];j--)这个倒叙循环的原因
(1)逻辑分析:二维数组第i行的数据要依据i-1行的数据,就意味着,dp[4]要依据上一次未改变的dp[3],dp[1],dp[0],但是如果从前向后遍历,是不是就改变了dp[0].dp[1],dp[3]的数据,就意味着计算dp[4]时,用的是本层的新的数据,而不是上一层未改变的数据,所以就要倒叙;
(2)理论分析:如果从前往后遍历,
dp[1]=max(dp[1],dp[0]+15)=15;dp[2]=max(dp[2],dp[1]+15)=30
就会发现,物品0被放了两次,不对!
好啦,本文就结束了,祝大家每天开开心心快快乐乐!!