0-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) ==》不一定装满背包

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

发布了28 篇原创文章 · 获赞 4 · 访问量 1210

猜你喜欢

转载自blog.csdn.net/weixin_41888257/article/details/104318415