0-1背包问题
这种情况下的背包问题是,给定物品种类N和背包重量W:
- 每个物品只能拿或者不拿,不存在只拿部分的情况
- 每个物品只有一个,不会有无数个或者多个
可用动态规划来求解。
先考虑用二维数组的解法:
行表示物品,如:第当i=3时,表示开始考虑前三个物品的情况。
列表示背包的重量,从0一直到W,如:当j=5时,表示当前背包重量是5。
行和列在一起的意义:当i=3,j=5时,表示在背包容量是5的情况下,装前三种物品,怎么装能价值最大。
分析:对于一个物品,对付他只有两个办法,to be or not to be,装还是不装?
- 装:这说明当前这个物品我要了,那么背包的容量就要变少,要变少多少?应该是当前物品的重量。举例来说, 原来背包有10kg,碰见了个3kg的物品,我选择装,那么自然就剩下10-3=7kg
- 不装:这说明这个物品我不要,那么背包容量就不必减少
所以,对于一个物品,我们总想看看它装入背包后和不装入的背包的总价值哪个大。这么说可能有点抽象,举例来说:假设背包容量是10kg,现在有一个物品,重7kg,价值是5块钱,由于这是第一个碰见的物品,自然要装(不装总价值就是0了,显然不对);又碰见另一个物品,重量是5kg,价值是4块钱,此时我们能想到的就是最好都能装进去,这样价值肯定最大,但是7+5=12>10,显然不能这么干,所以有如下考虑:
- 装:既然选择装第二个物品,那么第一个物品就不能要了,此时总价值是4
- 不装:选择不装,那么第一个物品就不必拿出,总价值是5
所以,应该选择不装,动态规划递推式如下:
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i-1]] + value[i-1])
用这个公式来理解上面的例子,当i=2,j=7时表示背包容量是7,目前遇见了两个物品,怎么装使总价值最大?
dp[2][7] = max ( dp[1][7] , dp[1][7-5] + 4 )
不选第二个物品对应dp[1][7] , 选对应dp[1][7-5] + 4,意思是选了第二个之后,背包的容量由7变为2,但是价值多了4,我们只需要知道dp[1][2]是多少就行,而这个在之前已经算出来了。
import numpy as np
def solution(max_weight,weight,value):
dp = np.zeros((len(weight)+1,max_weight+1),dtype=int)
for i in range(1,len(weight)+1):
for j in range(1,max_weight+1):
if j >= weight[i-1]:
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i-1]] + value[i-1])
else:
dp[i][j] = dp[i-1][j]
print(dp)
return dp
def things(max_weight,dp,weight,value):
goods = []
raw = len(weight)
col = max_weight
remain = dp[raw][col]
while remain != 0:
if dp[raw][col] != dp[raw-1][col]:
remain -= value[raw-1]
col -= weight[raw-1]
goods.append(raw)
raw -= 1
print('装入的物品是:',goods)
weight = [7,4,3,2]
value = [9,5,3,1]
dp = solution(10,weight,value)
things(10,dp,weight,value)
程序运行结果如下:
[[ 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 9 9 9 9]
[ 0 0 0 0 5 5 5 9 9 9 9]
[ 0 0 0 3 5 5 5 9 9 9 12]
[ 0 0 1 3 5 5 6 9 9 10 12]]
装入的物品是: [3, 1]
两个一维数组的解法:
上一节说的是是用二维数组来求解,这种方法的优点是可以找到装了哪些物品,缺点是占用的空间太大,所以如果不必知道装了哪些物品,使用下面的方式更为合适:
import numpy as np
import copy
def solution2(max_weight,weight,value):
dp = np.zeros(max_weight+1,dtype=int)
dp_next = np.zeros(max_weight+1,dtype=int)
for i in range(0,len(weight)):
for j in range(0,max_weight+1):
if weight[i] <= j:
dp_next[j] = max(dp[j],dp[j-weight[i]] + value[i])
dp = copy.copy(dp_next) # 其实这是浅拷贝,但是由于里面没有复杂元素,因此起到的作用和深拷贝一样
print(dp_next)
weight = [7,4,3,2]
value = [9,5,3,1]
输出结果:
[ 0 0 1 3 5 5 6 9 9 10 12]
分析程序:查看结果发现,这其实就是二维数组方法输出的最后一行,在二维数组中,下一行要用到上一行的数据。
举例来说:
- dp和dp_next开始都是0,然后进入循环,一开始i=1,表示只有一个物品,因为第一个物品重7kg,所以j从0到6都装不下,因此都是0,从j=7开始,可以装下第一个物品,所以总价值变为9,一直到j=10,总价值都是9,所以这个程序i=1执行之后dp_next应该是:[ 0 0 0 0 0 0 0 9 9 9 9]
- 将dp_next赋值给了dp,这就相当于用dp来存储i=1的整行数据,然后i=2,表示现在有2个物品可供挑选,由于第二个物品重量是4,因此j从0到3都不行,dp_next(注意,现在的dp_next是i=2这层的)从j=4到6价值都是5,到了j=7就要做抉择了,装第二个物品还是不装?
dp_next[j] = max(dp[j],dp[j-weight[i]] + value[i]),若不装,就是dp[j](因为dp存的是上一次,也就是i=1时的信息,相当于二维数组里面的dp[i-1][j]);若装,就是dp[j-weight[i]] + value[i](相当于二维数组里面的dp[i-1][j-w[i]] + value[i])。其实就是少了一个[i-1],在这里用dp来替代了而已。这次的执行结果是:[ 0 0 0 0 5 5 5 9 9 9 9] - 同理,i=3表示现在可以选三个物品,结果应该是:[ 0 0 0 3 5 5 5 9 9 9 12]
- [ 0 0 1 3 5 5 6 9 9 10 12]
这种方法其实就是舍弃了寻找哪些物品被装入来换取空间,由于下一行只需要用到上一行的信息,因此用两个一维数组反复迭代即可。
只用一个一维数组
回顾以上两种解法,新的一行都要用到上一行的信息,和本行的信息没有关系,举例来说:新的一行j=7时,只会用到上一行j=7之前的信息,本行和上一行j=7之后的信息都不会被使用到,因此可用如下代码来解决:
def solution3(max_weight,weight,value):
dp = np.zeros(max_weight+1,dtype=int)
for i in range(0,len(weight)):
for j in range(max_weight,-1,-1):
if j >= weight[i]:
dp[j] = max(dp[j],dp[j-weight[i]] + value[i])
print(dp)
weight = [7,4,3,2]
value = [9,5,3,1]
结果是:
[ 0 0 1 3 5 5 6 9 9 10 12]
分析程序:注意j是从大到小的,这非常重要,因为如果从小到大,即
for j in range(0,max_weight+1):
if j >= weight[i]:
dp[j] = max(dp[j],dp[j-weight[i]] + value[i])
那么程序就会出错,不妨来分析一下:
- 当i=1执行结束之后,dp的结果是[ 0 0 0 0 0 0 0 9 9 9 9],似乎没什么不对,接着往下看
- 当i=2,由于j从0开始,因此直到j=4之前,if语句都不会执行,到j=4后,dp[4] = max(dp[4],dp[4-4] + 5) = 5 [ 0 0 0 0 5 0 0 9 9 9 9]
j=5,dp[5] = max(dp[5],dp[5-4] + 5) = 5 [ 0 0 0 0 5 5 0 9 9 9 9]
j=6,dp[6] = max(dp[6],dp[6-4] + 5) = 5 [ 0 0 0 0 5 5 5 9 9 9 9]
j=7,dp[7] = max(dp[7],dp[7-4] + 5) = 9 [ 0 0 0 0 5 5 5 9 9 9 9]
注意!!!
j=8,dp[8] = max(dp[8],dp[8-4] + 5) = 10 [ 0 0 0 0 5 5 5 10 9 9 9]
发现什么不对没有?别说j=8,就算j=10都不可能出现前两种物品总价值是10的情况,为什么?
因为前面说过,“新的一行都要用到上一行的信息,和本行的信息没有关系”,如果你想让j从0开始,就破坏了这个原则。举例来说:假设j=4时更新了dp[4]=5,那么再到j=8时,dp[8-4]=dp[4]就不是上次i循环的信息了,而是这次更新过的信息;但是如果j从10开始,假设当前是第二次循环(物品2重4kg,价值是5),dp[10-4]=dp[6]用的就是第一次循环中的dp[6],因为本次循环的dp[6]还没有计算出来。