注意:for j in range(1,m+1):是枚举所有的情况,不用一一判断放入物品后背包容量减少后j的变化。为什么从1开始,因为0已经写出来了,即dp[i-1][j]=dp[i-1][j-0*a[i-1]]+0*v[i-1]。
01背包无价值
dp定义:前i项物品中的所有排序对应的当前重量(由给定数组决定)就为j的值,将此dp[i][j]=True表示就给的数组的前i项可以组成重量为j的组合。自然dp[i][0]都是True,不用放入物品自然是0。
for循环从m开始是因为题目要求最多装多少,故先从最大的开始。
dp=[[False]*(m+1) for i in range(n+1)]
dp[0][0]=True
for i in range(1,n+1):
dp[i][0]=True
for j in range(1,m+1):
if j-a[i-1]>=0:
dp[i][j]=dp[i-1][j] or dp[i-1][j-a[i-1]]
else:
dp[i][j]=dp[i-1][j]
for i in range(m,-1,-1):
if dp[n][i]==True:
return i
break
01背包有价值
dp=[[0]*(m+1) for i in range(n+1)]
#dp[0][j]=0,初始化前0项的价值为0
for i in range(1,n+1):
for j in range(1,m+1):
if j-a[i-1]>=0:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-a[i-1]]+v[i-1])
else:
dp[i][j]=dp[i-1][j]
return dp[n][m]
完全背包问题
max(dp[i-1][j],dp[i-1][j-a[i-1]]+v[i-1])实际上可以写成max(dp[i-1][j-0*a[i-1]]+0*v[i-1],dp[i-1][j-1*a[i-1]]+1*v[i-1])
依次类推多重背包就可以写成:
for count in range(j//a[i-1]+1):
max(dp[i-1][j-0*a[i-1]]+0*v[i-1],dp[i-1][j-1*a[i-1]]+1*v[i-1],dp[i-1][j-2*a[i-1]]+2*v[i-1]......)
n=len(a)
dp=[[0]*(m+1) for i in range(n+1)]
for i in range(1,n+1):
for j in range(m+1):
for count in range(j//a[i-1]+1):
dp[i][j]=max(dp[i][j],dp[i-1][j-count*a[i-1]]+count*v[i-1])
return dp[n][m]
但是会超时
优化
放第i件物品时。这里的处理和01背包有所不同,因为01背包的每个物品只能选择一个,因此选择放第i件物品就意味着必须转移到dp[i-1][v-w[i]]这个状态;
但是完全背包不同,完全背包如果选择放第i件物品之后并不是转移到dp[i-1][v-w[i]],而是转移到dp[i][v],这是因为每种物品可以放任意件(注意有容量的限制,因此还是有限的),放了第i件物品后还可以继续放第i件物品,直到第二维的v-w[i]无法保持大于等于0为止。
注意(for j in range(a[i-1],m+1):整个过程表示当前i-1个商品选完时该选则第i个商品,a[i-1]一直放,每一次放的次数不同所对应的j不同,直到到装不下时(即j<a[i-1]) ,而且每一次都是根据第i-1个商品所得到的关于j的一维列表得到的,也是一个i个商品对应的关于j一维列表。
关于状态方程为什么是dp[i][j]=max(dp[i-1][j],dp[i][j-a[i-1]]+v[i-1]),因为dp[i][j]是先从dp[i-1][j]转移过来,然后再进行a[i][j]的数量选择,即dp[i][j-a[i-1]]。
n=len(a)
dp=[[0]*(m+1) for i in range(n+1)]
for i in range(1,n+1):
for j in range(a[i-1],m+1):
dp[i][j]=max(dp[i-1][j],dp[i][j-a[i-1]]+v[i-1])
return dp[n][m]
多重背包
我们考虑一下,如果所有ni都满足ni ≥ W / wi,那不就变成完全背包的问题了么。可见,完全背包的基本实现思路也可以应用到多重背包的基本实现。对于多重背包的基本实现,与完全背包是基本一样的,不同就在于物品的个数上界不再是v/c[i]而是n[i]与v/c[i]中较小的那个。所以我们要在完全背包的基本实现之上,再考虑这个上界问题。
这次我们直接空间优化,不再讲解二维做法:
多重背包是可以不选,也可以选 1 个,可以选多个,而 0-1 背包只能选 0 个或者 1 个。
那就直接把种物品分开,即可比如:
每个盘子 3 块钱,我有 2 个。每双筷子 1 块钱,我有 10 双,每对刀叉 3 块钱,我有 3 个。
那么我就可以拆成,有 2 个三块的盘子,每个可以选也可以不选,就变成了 0-1 背包。
也就是说,对于每种是可以选多个,那就直接拆分成独立的个体就可以了。
代码与完全背包的区别除了多了个表示物品个数的数组s[ ]之外,只在内循环的控制条件那里。
n=len(a)
dp=[[0]*(m+1) for i in range(n+1)]
for i in range(1,n+1):
for j in range(1,m+1):
for k in range(min(j//a[i-1]+1,s[i])+1)):
dp[i][j]=max(dp[i][j],dp[i-1][j-k*a[i-1]]+k*v[i-1])
return dp[n][m]
滚动数组(空间优化(二维变一维)
01背包
因为状态转移每次只与上一层有关,所以用一个一维数组就可以。
为什么从大到小遍历?
看 dp[j]=dp[j-c[i]]+w[i]这一状态转移,
为什么从c[i]开始,是因为j不能小于c[i]
为什么是逆序呢,因为在第一次i=0的for循环时,所有满足range(c[i],C+1)的dp[j]都是w[I],所有的dp[j - c[i]]都是0,因为第一次之前的价值均为0,
可能有人要问了,第一次的j难道不都是C吗,是都是,但是其他不是C的但是大于c[i]的可以不用管。
如果从小到大则当循环到大的j时,dp[j - c[i]]不一定等于0。
是根据小的改大的,如果先把小的改了,那小的还会被用到,数据就不对了,所以从小到大。
为什么是逆序遍历?,这个是根据原状态方程判断的由dp[i][j]=max(dp[i-1][j],dp[i-1][j-a[i-1]]+v[i-1])可知,dp[i][j]是由上方i-1层和左边一直到j为止的一维数据转换而来,变到一维dp[j] = max(dp[j - a[i]] + v[i], dp[j])则是由之前i-1次的一维列表的转换而来,因为是靠旧的且是左侧的数据,所以就要从大到小开始遍历(如果是靠右边的旧数据,则是从小到大)
注意到状态转移方程中计算dp[i][v]时总是只需要dp[i-1][v]左侧部分的数据(即只需要图中正上方与左上方的数据),且当计算dp[i+1][ ]的部分时, dp[i-1]的数据又完全用不到了(只需要用到dp[i][ ]),因此不妨可以直接开一个一维数组dp[v](即把第一维省去),枚举方向改变为i从1到n,v从V到0(逆序!),
for i in range(n):
for j in reversed(range(a[i],m+1)):
dp[j] = max(dp[j - a[i]] + v[i], dp[j])
print(dp[m])
完全背包
空间优化:
未省略取物品次数k的等价转换代码:
for i in range(1, N + 1):
for j in reversed(range(a[i],m+1)):
for k in range(0, j // a[i] + 1):
dp[j] = max(dp[j], dp[j - k * a[i]] + k * v[i])
print(dp[m])
因为状态转移每次只与上一层有关,所以用一个一维数组就可以。
为什么这次是从小到大遍历了呢?
是根据小的改大的,而此时的含义为选了 x 件后的容量与质量,跟第一个类似,但含义不同,所以子处理方式上也有本质区别,处理完一件后在处理下件。
完全背包的一维数组实现和01背包也是几乎完全相同,唯一差别是完全背包的内循环是正向遍历,而01背包的内循环是逆向遍历。
省略取物品次数k的等价转换代码:
为什么是又变成了正序,这次还看原状态方程dp[i][j]=max(dp[i-1][j],dp[i][j-a[i-1]]+v[i-1]),可知dp[i][j]是由右上一层即i-j层正上方(在一维中直接是dp[j] =dp[j])和右测以j开头的数据得到因为dp[i][j]=dp[i][j-a[i-1]]中左侧的j小于右测的j。故一维状态方程为正序。
怎么理解必须正向枚举呢?求解dp[i][v]需要它左边的dp[i][v-w[i]]和它上方的dp[i-1][v],显然如果让v从小到大枚举,dp[i][v-w[i]]就总是已经计算出的结果;而计算出dp[i][v]之后dp[i-1][v]就再也用不到了,可以直接覆盖。
为什么可以覆盖,因为物品可以多次取出,每多取一次都是在覆盖。
for i in range(n):
for j in range(a[i],m+1):
dp[j] = max(dp[j - a[i]] + v[i], dp[j]);
print(dp[m])
多重背包
还是看原状态方程dp[i][j]=max(dp[i][j],dp[i-1][j-k*a[i-1]]+k*v[i-1])
for i in range(n):
for j in reversed(range(a[i],m+1)):
for k in range(s[i]+1):
if j<k*a[i]:
break
dp[j] = max(dp[j], dp[j - k * a[i]] + v[i] * k)
print(dp[m])