对于动态规划算法的基础学习(2)

97 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

背包问题:0-1背包,完全背包,多重背包,混合三种背包,二维费用背包等等;

0-1背包:有n种物品,每种物品只有一个

完全背包:有n种物品,每种物品有无限个

多重背包:有n种物品,每种物品的个数各不相同

主要体现在物品数量的不同

本文先从最简单的0-1背包问题开始研究##

问题描述:有N件物品和一个可以容纳重量为W的背包,第i件物品的重量是weight[i],价值是value[i],求解应在背包中放入哪些物品使得背包的总价值最大,并且需要满足总重量不超过背包的最大重量。

首先,如果不考虑动态规划,最简单的想法就是:

每种物品都有两种状态,放入背包or不放入背包,所以只需要遍历所有的情况,选择价值最大的那种便好,此时算法的复杂度为O(n2)。

下面,我们来一起看动态规划:

以一个简单的例子学习:假设此时背包能够容纳最大的重量为4

物品重量价值
0115
1320
2430

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

i01234
0015151515
10    
20    

如上表所示,这部分数赋予初值需要考虑,第一列表示的含义为,背包容量为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]
00000

按照遍历顺序:

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被放了两次,不对!

好啦,本文就结束了,祝大家每天开开心心快快乐乐!!