您的位置:首页 > 编程语言 > Python开发

0-1背包问题,使用动态规划的三种方法(二维数组,两个一维数组,一个一维数组)python实现

2020-06-04 07:09 731 查看

0-1背包问题

感谢这些朋友们的文章,给了我很大启发:
https://blog.csdn.net/songyunli1111/article/details/94778914
https://blog.csdn.net/na_beginning/article/details/62884939
https://blog.csdn.net/qq_39445165/article/details/84334970

这种情况下的背包问题是,给定物品种类N和背包重量W:

  1. 每个物品只能拿或者不拿,不存在只拿部分的情况
  2. 每个物品只有一个,不会有无数个或者多个

可用动态规划来求解。

先考虑用二维数组的解法:

行表示物品,如:第当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]

分析程序:查看结果发现,这其实就是二维数组方法输出的最后一行,在二维数组中,下一行要用到上一行的数据。
举例来说:

  1. 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]
  2. 将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]
  3. 同理,i=3表示现在可以选三个物品,结果应该是:[ 0 0 0 3 5 5 5 9 9 9 12]
  4. [ 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])

那么程序就会出错,不妨来分析一下:

  1. 当i=1执行结束之后,dp的结果是[ 0 0 0 0 0 0 0 9 9 9 9],似乎没什么不对,接着往下看
  2. 当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]还没有计算出来。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐