完全背包问题(python实现)的几种解法,动态规划

完全背包问题

感谢这些朋友们的文章,给了我很大启发:
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

这个问题和上篇01背包问题的区别就在于:完全背包问题中的物品数是无穷多个,也就是说对于一个物品,我可以不拿,可以拿一个、两个…只要背包空间够,理论上可以拿无限多个。

下面用几种方法来解决这个问题:

第一种使用二维数组的方法

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][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):
    raw = len(weight)
    col = max_weight
    remain = dp[raw][col]
    goods = []

    while remain != 0:
        if dp[raw][col] != dp[raw-1][col]:
            remain -= value[raw-1]
            col -= weight[raw-1]
            goods.append(raw)
            raw += 1

        raw -= 1
    print(goods)    

数据是:
weight = [7,4,3,2]
value = [9,5,3,1]

程序的运行结果:

[[ 0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  1  1  2  2  3  3  4  4  5]
 [ 0  0  1  3  3  4  6  6  7  9  9]
 [ 0  0  1  3  5  5  6  8 10 10 11]
 [ 0  0  1  3  5  5  6  9 10 10 12]]
[4, 2]

分析程序:这段代码和上一篇01背包问题使用二维数组的代码很像,唯一的不同就是:

dp[i][j] = max(dp[i-1][j],dp[i][j-weight[i-1]] + value[i-1])

而在上一篇中是这样的:

dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i-1]] + value[i-1])

为什么稍稍一改就可以解完全背包问题了?
对于一个物品,我们可以选择不装、装1个、2个…那么在程序里
dp[i-1][j]对应着不装
dp[i][j-weight[i-1]] + value[i-1] 对应装至少一个,举例来说:

  1. 现在有物品1重5kg,价值5,那么j从0开始,一直到4,dp[1][j]都是0。现在j = 5,不用多说,肯定要装(因为目前i=1,只有第一个物品,不装总价值就是0),那么dp[1][5] = 5,dp[1][6] = 5,…到j = 10时,由于可以装无限多个,又考虑到背包总容量是10kg,所以第一轮循环结束后,总价值dp[1][10] = 10。

  2. 第二轮循环,碰见了物品2,重3kg,价值3,那么dp[2][0],dp[2][1],dp[2][2]都是0,dp[2][3]=3,dp[2][4]=3(因为容量没到5,只能装物品2),注意,到j = 5了,你面临抉择:装物品1还是物品2?

    • 若装物品1,那么总价值就是5,剩余容量是5-5=0
    • 若装物品2,那么总价值就是3,剩余容量是5-3=2

    很显然,在j = 5的情况下,应该选择物品1,即dp[2][5] = 5

    j = 6,又要面临抉择,选1还是2?

    • 若选1,那么总价值就是5,剩余容量是6-5=1
    • 若选2,那么总价值就是 3 + dp[2][6-3],那dp[2][3]等于多少?前面已经算了,是3。
    • 所以j = 6时最优方案是选一个物品2,再选一个物品2,因为dp[2][3]=3,所以总价值是6,背包的剩余容量是0。

    j=7,同样,两个选择:

    • 选物品1,总价值就是5,剩余容量是7-5=2
    • 选物品2,那么总价值就是 3 + dp[2][7-3],那dp[2][4]等于多少?前面已经算了,是3。
    • 所以选2的总价值是6,背包的剩余容量是1。

    j=8:

    • 选物品1,总价值就是5,剩余容量是8-5=3
    • 选物品2,那么总价值就是 3 + dp[2][8-3],那dp[2][5]等于多少?前面已经算了,是5。
    • 所以j = 8时的最优方案是选一个物品2之后,再选一个物品1,总价值是3 + 5 = 8,剩余容量0。

    j=9:

    • 选物品1,总价值就是5,剩余容量是9-5=4
    • 选物品2,那么总价值就是 3 + dp[2][9-3],那dp[2][6]等于多少?前面已经算了,是6。
    • 所以j = 9时的最优方案是选一个物品2之后,再选2个物品2,总价值是3 + 6 = 9,剩余容量0。

    j=10:

    • 选物品1,总价值就是10,剩余容量是0
    • 选物品2,那么总价值就是 3 + dp[2][10-3],那dp[2][7]等于多少?前面已经算了,是6。
    • 所以j = 10时的最优方案是选2个物品1,总价值是5 + 5 = 10,剩余容量0。

    如果还是没搞懂的话,不妨这么想:
    01背包问题的动态规划公式是:

    dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i-1]] + value[i-1])
    

    如果我装了当前物品,由于只能装一个,那么装完这个就只能看看剩下的空间还能不能装前面的物品,对不?举例来说:现在有10kg空间,碰见了第二个物品5kg,我选择装它,那么我接下来要做的就是看看背包目前的容量(10-5=5kg)能不能装得下第一个物品。
    那回到完全背包问题,一个物品可以装无数次,那么我们可以在装完一个物品之后看看剩下的容量能不能再装下这个物品,而不是立刻回到前面的物品,所以这种情况的公式是 dp[i][j-weight[i-1]] + value[i-1]

    所以上述的代码能够求解完全背包问题。

在其他的博客里看见有人给二维数组的第一行进行了初始化,就是看看第一个物品能被j从0到max装下多少,例如:物品重3kg,max是10kg,那么初始化之后第一行就是[0 0 0 3 3 3 6 6 6 9 9],这样做的原因是他的二维数组只有和物品种类数目相同的行数,而我们的程序行数是物品种类数目+1。我把代码也贴在下面了:

def solution(max_weight,weight,value):
    dp = np.zeros((len(weight),max_weight+1),dtype=int)
    for j in range(1,max_weight+1):
        dp[0][j] = 0 if j < weight[0] else j//weight[0]

    for i in range(1,len(weight)):
        for j in range(1,max_weight+1):
            if j >= weight[i]:
                dp[i][j] = max(dp[i-1][j],dp[i][j-weight[i]] + value[i])
            else:
                dp[i][j] = dp[i-1][j]

    print(dp)
    return dp

weight = [2,3,4,7]
value = [1,3,5,9]

第二种方法

这种方法相对来说好理解,思路是这样的:
既然每个物品可以拿无限多个,那么可以这样:当前物品i先拿0个,剩下的空间装前i-1个,计算一下总价值保存下来;当前物品i先拿1个,剩下的空间装前i-1个,计算一下总价值保存下来;…,直到当前物品的 数量*重量 超过背包容量为止,代码如下:

def solution2(max_weight,weight,value):
    dp = np.zeros((len(weight)+1,max_weight+1),dtype=int)
    max_num = 0
    for i in range(1,len(weight)+1):
        for j in range(0,max_weight+1):
            k = 0
            while k * weight[i-1] <= j:
                t = k * value[i-1] + dp[i-1][j - k * weight[i-1]]
                if t > max_num:
                    max_num = t
                k += 1

            dp[i][j] = max_num
            max_num = 0
    print(dp)

数据是:
weight = [2,3,4,7]
value = [1,3,5,9]
最大容量为10

第三种方法,只用一个一维数组

def solution3(max_weight,weight,value):
    dp = np.zeros(max_weight+1,dtype=int)

    for i in range(0,len(weight)):
        j = weight[i-1]
        while j <= max_weight:
            dp[j] = max(dp[j] , dp[j-weight[i-1]] + value[i-1])
            j += 1
    print(dp)

数据是:
weight = [2,3,4,7]
value = [1,3,5,9]
最大容量为10

运行结果如下:

[ 0  0  1  3  5  5  6  9 10 10 12]

这种方法可能有些难理解,和01背包问题中一个一维数组的解法非常相似,只是j的遍历方式不同,原因在于,01背包问题中,本行的信息只用得到上一行的信息,和本行的信息无关。举例来说:假设现在是第3行(i=3),j到了6,那么01背包问题只会用到第二行(i=2)和j=6之前的信息,和第三行j=6之前的信息无关;但是完全背包问题可以重复拿,所以j要从小到大遍历,这样就能取得更新后的信息。

将这种情况和这篇文章第一个方法、01背包问题中一个一维数组的方法放在一起看可能会更加清晰。

若还是不清楚,多用笔在纸上演算一下程序运行过程,很快就熟悉算法的原理了。

你可能感兴趣的:(leetcode,Python,算法)