背包问题详解

更新!!!

1. 0-1背包

题目:有 N 件物品和容量为 W 的背包。第 i 件物品的重量为 w_i,价值为 v_i,求将不超过背包容量的物品装入背包能得到的最大价值。

特点,每件物品的数量只有一个,可以选择放或不放某件物品。

d p [ i ] [ j ] dp[i][j] dp[i][j] 表示将前 i + 1 i+1 i+1 件总重量不超过 j j j 的物品放入背包能获得的最大价值,则可以用以下的转移方程来表示这个过程:

d p [ i , j ] = m a x ( d p [ i − 1 , j ] , d p [ i − 1 , j − w [ i ] ] + v [ i ] ) dp[i,j] = max(dp[i - 1, j], dp[i-1, j-w[i]] + v[i]) dp[i,j]=max(dp[i1,j],dp[i1,jw[i]]+v[i])

注意到 dp 数组第i行的值更新只跟 i − 1 i-1 i1 行有关,因此可以通过滚动数组或者反向更新的方式优化一下空间复杂度,在动态规划解题的时候这是一种常用的空间复杂度优化方式。优化后的代码如下:

def knapsack_1D_1(N, W, w, v):
    """ 0-1 背包 一维 dp """
    dp = [0] * (W + 1)
    for i in range(0, N):
        for j in range(W, -1, -1):  # [20,...,0]
            if w[i] <= j:
                dp[j] = max(dp[j], dp[j - w[i]] + v[i])

    print("dp:\n", dp)
    print("dp[-1]:\n", dp[-1])

if __name__ == '__main__':
    N = 3   # 物品数量
    W = 10  # 背包容量
    #n = [2,1,2]
    w = [2,5,4]
    v = [3,10,12]
    knapsack_1D_1(N, W, w, v)

2. 完全背包

题目:有 N 种物品和容量为 W 的背包。第 i 种物品的重量为 w_i,价值为 v_i,每种物品的数量无限。求将不超过背包容量的物品装入背包能得到的最大价值。

特点:每种物品的数量无限多

考虑到每种物品的数量无限。用 d p [ j ] dp[j] dp[j] 表示在重量不超过 j j j 的情况下背包中物品可以达到的最大价值,则转移方程如下:

d p [ j ] = m a x ( d p [ j ] , d p [ j − w [ i ] ] + v [ i ] ) dp[j]=max(dp[j], dp[j-w[i]]+v[i]) dp[j]=max(dp[j],dp[jw[i]]+v[i])

def knapsack_1D_2(N, W, w, v):
    """ 完全 背包 一维 dp """
    dp = [0] * (W + 1)
    for i in range(0, N):
        for j in range(w[i], W + 1):
                dp[j] = max(dp[j], dp[j - w[i]] + v[i])

    print("dp:\n", dp)
    print("dp[-1]:\n", dp[-1])

if __name__ == '__main__':
    N = 3   # 物品数量
    W = 10  # 背包容量
    # n = [2,1,2]
    w = [2,5,4]
    v = [3,10,12]

    knapsack_1D_2(N, W, w, v)

注意内层for循环是从前向后更新 dp 数组的,这是唯一和上面的 0-1 背包问题区别的地方。原因在于,题目中每种物品的数量无限多,在放入一件物品 i 时,要考虑之前已经放过物品 i 的情况。

3. 多重背包

题目:有 N 种物品和容量为 W 的背包。第 i 种物品的数量有n_i个,每个物品重量为 w_i,价值为 v_i,每种物品的数量无限。求将不超过背包容量的物品装入背包能得到的最大价值。

特点:每种物品的数量不止一个,但有限

基本的多重背包问题状态转移方程如下:

d p [ j ] = m a x ( d p [ j ] , d p [ j − k ∗ w [ i ] ] + k ∗ v [ i ] ) , 0 ≤ k ≤ n [ i ] dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]), 0\leq k \leq n[i] dp[j]=max(dp[j],dp[jkw[i]]+kv[i]),0kn[i]

def knapsack_1D_3(N, W, n, w, v):
    """ 多重 背包 一维 dp """
    dp = [0] * (W + 1)
    for i in range(0, N):
        for  j in range(W, w[i] - 1, -1):
            for k in range(1, n[i] + 1):
                if (j < k * w[i]):
                    break
                dp[j] = max(dp[j], dp[j - w[i] * k] + v[i] * k)

    print("dp:\n", dp)
    print("dp[-1]:\n", dp[-1])

if __name__ == '__main__':
    N = 3   # 物品数量
    W = 10  # 背包容量
    n = [2,1,2]
    w = [2,5,4]
    v = [3,10,12]
    knapsack_1D_3(N, W, n, w, v)

参考:

  1. 背包问题解题方法总结

======================================================================================================================================

0-1背包问题详解(旧版本)

0-1 背包问题:小偷有一个固定容量的背包,到商店偷东西,每个商品的重量 weight(w)和价格 value (v)都不同。问怎么规划能够偷到最多价格的商品?

B(k,c) 表示当背包剩余容量为 c 的时候,偷 [1,…,k]这前 k 件物品的时候,能够获取的最大价值。

"""
参考: 
https://www.bilibili.com/video/av36136952?from=search&seid=9381742344384892478
https://blog.csdn.net/TimeMagician/article/details/79731062
https://www.acwing.com/solution/acwing/content/1374/

【公式】

k表示商品序号

         |------>  B(k-1,c) 第k件太重     (1
B(k,c) = |
         |  -> 偷: B(k-1,c-w(k)) + v(k)  (2
         |--|
            -> 不偷:B(k-1,c)             (3

【动态规划解法】
===============================================================================================================
            [index(k)]
[capacity(c)]            0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17  18  19 20
  no item       0        0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0  0
w 2   v 3       1        0   0   3   3   3   3   3   3   3   3   3   3   3   3   3   3   3   3   3   3  3
w 3   v 4       2        0   0   3   4   4   7   7   7   7   7   7   7   7   7   7   7   7   7   7   7  7
w 4   v 5       3        ...
w 5   v 8       4        ...
w 9   v 10      5        ...                                                                            26

==============================================================================================================

"""
import numpy as np


def knapsack():
    """ 二维 dp """
    N = 5   # 物品数量
    W = 20  # 背包容量
    B = np.zeros((N+1, W+1))
    w = [0, 2, 3, 4, 5, 9]      # 前面补上0是为了和数组对其,no item 等价于在上面的二维数组中 w0 v0
    v = [0, 3, 4, 5, 8, 10]
    # k: [1,...,5]
    for k in range(1, N+1):
	    # c : [0,...,20]/[1,...,20] 从0或者1开始都可以  c 表示 capacity
	    # 如果初始化的时候,只初始化第0行全为0,而不初始化第0列全为0的话,那么可以从(k,c)=(1,0)开始就有意义了,因为(1,0)还是未知数
	    # 但是如果初始化的时候,已经把整个二维数组都初始化为0的话,那么也就是 第0行和第0列都是全0,那么直接从(1,1)开始就好,也就是c=1开始
        for c in range(0, W+1):
            if w[k] > c:  # 如果第k个的重量已经太重了(第 1)种情况)
                B[k][c] = B[k - 1][c]
            else:

                B[k][c] = max(B[k-1][c], B[k - 1][c - w[k]] + v[k])

    print("======================= 2D ============================")
    print("B:\n",B)
    print("B[5][20]:\n",B[5][20])   # answer = 26

def knapsack_1D():
    """ 一维 dp """
    N = 5   # 物品数量
    W = 20  # 背包容量
    B = np.zeros((W+1))
    w = [0, 2, 3, 4, 5, 9]      # 前面补上0是为了和数组对其,no item 等价于在上面的二维数组中 w0 v0
    v = [0, 3, 4, 5, 8, 10]
    # k: [1,...,N]
    for k in range(1, N+1):     # [1,...,5]
        for c in range(W, -1, -1): # [20,...,0]

            # if w[k] > c:  # 如果第k个的重量已经太重了(第 1)种情况)
            #     B[c] = B[c]
            # else:
            #     B[c] = max(B[c], B[c - w[k]] + v[k])

            if w[k] <= c:
	            B[c] = max(B[c], B[c - w[k]] + v[k])

    print("======================= 1D ============================")
    print("B:\n",B)
    print("B[-1]:\n",B[20])   # answer = 26




if __name__ == '__main__':

    knapsack()
    knapsack_1D()

【输出】

D:\software_install_position\anaconda3\envs\tf_1_12_0\python.exe "E:/####学习资源/#工作资料 数据结构、计算机基础等/数据结构/刷题LeetCode/DP_Learning/knapsack.py"
======================= 2D ============================
B:
 [[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
   0.  0.  0.]
 [ 0.  0.  3.  3.  3.  3.  3.  3.  3.  3.  3.  3.  3.  3.  3.  3.  3.  3.
   3.  3.  3.]
 [ 0.  0.  3.  4.  4.  7.  7.  7.  7.  7.  7.  7.  7.  7.  7.  7.  7.  7.
   7.  7.  7.]
 [ 0.  0.  3.  4.  5.  7.  8.  9.  9. 12. 12. 12. 12. 12. 12. 12. 12. 12.
  12. 12. 12.]
 [ 0.  0.  3.  4.  5.  8.  8. 11. 12. 13. 15. 16. 17. 17. 20. 20. 20. 20.
  20. 20. 20.]
 [ 0.  0.  3.  4.  5.  8.  8. 11. 12. 13. 15. 16. 17. 17. 20. 20. 21. 22.
  23. 25. 26.]]
B[5][20]:
 26.0
======================= 1D ============================
B:
 [ 0.  0.  3.  4.  5.  8.  8. 11. 12. 13. 15. 16. 17. 17. 20. 20. 21. 22.
 23. 25. 26.]
B[-1]:
 26.0

Process finished with exit code 0

【特别说明】
一维的这种情况,这里的 c 倒序 是因为在二维表格中 B[k - 1][c - w[k]] 位于 B[k][c] 左上方。

类似于常见的 a[i][j]=a[i-1][j-1]的题目的中,要转成一维的话,由于 a[i-1][j-1]位于a[i][j]的左上方。如果 j 从小到大,即从左往右的话,会造成值的覆盖,所以需要搞一个临时变量 pre,当然如果初始化的值足够的话,也可以倒序。

之前这种题目一般不会出现 二维表的第0行整行初始值都知道,所以以往的题目都是用临时变量保存的 比如《No.72 编辑距离》。

而倒序就不会造成覆盖了,所以这里同理,c 倒序,从大到小,从右往左。

详见下图
在这里插入图片描述
在这里插入图片描述

【综上所述】

对于 将二维dp转化为一维dp的问题,其实就是利用一维数组代替二维数组,利用一维数组不断更新,而且更新需要保证不能将后续需要的数值覆盖。

以下分两类情况讨论。

1. 形如 a[ i ][ j ] = a[i - 1][ j ] (正上方) 或者 a[ i ][ j ] = a[ i ][j - 1] (正左侧)

这类问题在二维转一维的时候,可以直接丢弃第一个维度 i
即转化为 a[ j ] = a[ j ] 或者 a[ j ] = [ j - 1 ],因为不会出现值覆盖的情况

2. 形如a[ i ][ j ] = a[ i-1 ][ j-k ] (左上方),其中k >=1

这类问题有正序和逆序两种赋值方式。

2.1 正序(第二重循环 j )

1)采用临时变量pre。如上图所示,需要额外的临时变量 pre来保存即将被覆盖的数值。这样才能保证 一维的 a[ j ] = a[ j - k ]与原先二维的a[ i ][ j ] = a[ i - 1 ][ j - k ] 等价

2)不采用临时变量。如上图所示,如果直接正序赋值,会导致值覆盖。
一维的 a[ j ] = a[ j - k ]将等价于二维的a[ i ][ j ] = a[ i ][ j - k ] ,而非原先的a[ i ][ j ] = a[ i - 1 ][ j - k ]。

2.2 逆序(第二重循环 j )

逆序的前提是,二维数组的第 0 行,或者说一维数组的全部值的初始值都是已知的,这样从后往前就不会导致值的覆盖。这种思想在数组排序中有所体现,从后往前插入的话需要挪动的位置更少。

初始化的细节问题

我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求"恰好装满背包"时的最优解,有的题目则并没有要求必须把背包装满。这两种问法的区别是在初始化的时候有所不同。

如果是第一种问法,要求恰好装满背包,那么在初始化时除了 f [ 0 ] 为0,其它 f [1…V] 均设为−inf,这样就可以保证最终得到的 f [ N ] 是一种恰好装满背包的最优解。

如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将
f [ 0…V] 全部设为0。

为什么呢?可以这样理解:初始化的 f 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可能被价值为 0 的 nothing “恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是−inf 了。即:

1). f [ 0 ] == 背包容量 0 (合法)
2). f [ 1, …, V] == -inf < 背包容量 0 (不合法)

  1. 和2) ==》恰好装满背包

如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为 0 了。即:

1). f [ 0 ] == 背包容量 0 (合法)
2). f [ 1, …, V] == 0 == 背包容量 0 (合法)

  1. 和2) ==》不一定装满背包

这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值