献芹奏曝-Python面试题-算法-背包问题

上一篇:献芹奏曝-Python面试题 

      开篇的话:本文目的是收集和归纳力扣上的算法题,希望用python语言,竭我所能做到思路最清奇、代码最简洁、方法最广泛、性能最高效,了解常见题目,找到最利于记忆的答案,更加从容的应对面试。希望广思集益,共同进步。

一、0  1 背包

一言蔽之:每类物品数量只有一个,选还是不选,这是一个问题。To be, or not to be: that is the question。

 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

1:背包分析

1.1 问题描述

现在有4个物品,小偷背包最大容量为8,怎么可以偷得价值最多的物品?

物品编号 重量价值
1 2 3
2 3 4
3 4 5
4 5 8

 

1.2 二维dp数组解决背包问题

1.2.1.确定dp数组以及下标含义

通过使用二维数组dp[i][j]:表示从下标为[0-i]的物品里任意取,放入容量为j的背包,价值总和最大是多少。时刻要牢记这个dp数组的含义,

 背包重量 0背包重量 1背包重量 2背包重量 3背包重量 4背包重量 5背包重量 6背包重量 7背包重量 8
物品 1 0 0 3 3 3 3 3 3 3
物品 2 0                
物品 3 0                
物品 4 0                

1.2.2.确定递推公式

有两个方向可以推出来递推公式

  1. 不放物品i:由dp[i-1][j]推出,即背包容量为j,里面不放物品i的最大价值就是 dp[i-1][j]。这个其实很好理解,为什么不放物品i呢?放不下呗,背包容量剩余2当前物品i重量是3。那此时最大价值依然和前面相同。
  2. 放物品i:由dp[i-1][j]推出,即背包容量为j,里面不放物品i的最大价值就是 dp[i-1][j-weight[i]]+value[i]。这个其实也很好理解,放了物品i之后,物品i的重量是weight[i],价值是value[i]。那么背包的剩余重量就是j-weight[i]。那么一下子就回到了原来上一步的问题,求dp[i-1][j-weight[i]]的最大价值。

所以,递推公式是求放物品和不放物品时的最大值,即:dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])

1.2.3.dp数组初始化

上面的图表中已经初始化了背包重量为0和只放物品1时的数据。ps:初始化一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱

从递归公式:dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]) 可以看出来dp[i][j]是由左上方的数值推导出来的,空格中的数据其实是什么已不再重要,反正都会覆盖。

1.2.4.确定遍历顺序

问题来了,先遍历物品还是先遍历背包重量呢?其实都可以,最好是先遍历物品。

为什么都可以,其实很好理解。根据递推公式:取得是左上方数据。行列转换之后对其值无影响。

先遍历物品,其实就是按行慢慢填充;先遍历背包,其实就是按列慢慢填充。

1.2.5.代码示例

def bag_2_wei(bag_size, wp_info):
    dp = [[0] * (bag_size + 1) for j in range(len(wp_info))]
    # 01 初始化dp数组
    for i in range(len(dp)):
        # 初始化列:背包重量是0的价值
        dp[i][0] = 0
    first_item = wp_info.get(1)
    first_item_weight, first_item_value = first_item.get("weight"), first_item.get("value")
    for j in range(len(dp[0])):
        # 初始化行,只放第一个物品的价值
        if first_item_weight <= j:
            dp[0][j] = first_item_value

    # 02 更新数组:先遍历物品,再遍历背包
    for i in range(1, len(dp)):
        for j in range(1, len(dp[0])):
            # 物品的id是从1 开始,所以等于当前j+1
            cur_item = wp_info.get(i + 1)
            if cur_item.get("weight") > j:
                # 当前物品数量 比 背包数量大,说明背包装不下
                dp[i][j] = dp[i - 1][j]
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cur_item.get("weight")] + cur_item.get("value"))
        print(dp)


wp_info = {
    1: {"weight": 2, "value": 3},
    2: {"weight": 3, "value": 4},
    3: {"weight": 4, "value": 5},
    4: {"weight": 5, "value": 8},
}
bag_size = 8

bag_2_wei(bag_size, wp_info)

"""
[[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 7, 7], [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], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 0, 0, 0, 0, 0, 0, 0]]
[[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 8, 8, 11, 12]]
[[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 8, 8, 11, 12]]
"""
二维数组,先遍历物品
"""
01背包
"""


def bag_2_wei_wp_first(bag_size, wp_info):
    dp = [[0] * (bag_size + 1) for j in range(len(wp_info))]
    # 01 初始化dp数组
    for i in range(len(dp)):
        # 初始化列:背包重量是0的价值
        dp[i][0] = 0
    first_item = wp_info.get(1)
    first_item_weight, first_item_value = first_item.get("weight"), first_item.get("value")
    for j in range(len(dp[0])):
        # 初始化行,只放第一个物品的价值
        if first_item_weight <= j:
            dp[0][j] = first_item_value

    # 02 更新数组:先遍历物品,再遍历背包
    for i in range(1, len(dp)):
        for j in range(1, len(dp[0])):
            # 物品的id是从1 开始,所以等于当前j+1
            cur_item = wp_info.get(i + 1)
            if cur_item.get("weight") > j:
                # 当前物品数量 比 背包数量大,说明背包装不下
                dp[i][j] = dp[i - 1][j]
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cur_item.get("weight")] + cur_item.get("value"))
        print(dp)


def bag_2_wei_bag_first(bag_size, wp_info):
    dp = [[0] * (bag_size + 1) for j in range(len(wp_info))]
    # 01 初始化dp数组
    for i in range(len(dp)):
        # 初始化列:背包重量是0的价值
        dp[i][0] = 0
    first_item = wp_info.get(1)
    first_item_weight, first_item_value = first_item.get("weight"), first_item.get("value")
    for j in range(len(dp[0])):
        # 初始化行,只放第一个物品的价值
        if first_item_weight <= j:
            dp[0][j] = first_item_value

    # 02 更新数组:先遍历物品,再遍历背包
    for j in range(1, len(dp[0])):
        for i in range(1, len(dp)):
            # 物品的id是从1 开始,所以等于当前j+1
            cur_item = wp_info.get(i + 1)
            if cur_item.get("weight") > j:
                # 当前物品数量 比 背包数量大,说明背包装不下
                dp[i][j] = dp[i - 1][j]
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cur_item.get("weight")] + cur_item.get("value"))
        print(dp)


wp_info = {
    1: {"weight": 2, "value": 3},
    2: {"weight": 3, "value": 4},
    3: {"weight": 4, "value": 5},
    4: {"weight": 5, "value": 8},
}
bag_size = 8

bag_2_wei_bag_first(bag_size, wp_info)

"""
[[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 0, 0, 0, 0, 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], [0, 0, 3, 0, 0, 0, 0, 0, 0], [0, 0, 3, 0, 0, 0, 0, 0, 0], [0, 0, 3, 0, 0, 0, 0, 0, 0]]
[[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 0, 0, 0, 0, 0], [0, 0, 3, 4, 0, 0, 0, 0, 0], [0, 0, 3, 4, 0, 0, 0, 0, 0]]
[[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 0, 0, 0, 0], [0, 0, 3, 4, 5, 0, 0, 0, 0], [0, 0, 3, 4, 5, 0, 0, 0, 0]]
[[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 0, 0, 0], [0, 0, 3, 4, 5, 7, 0, 0, 0], [0, 0, 3, 4, 5, 8, 0, 0, 0]]
[[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 0, 0], [0, 0, 3, 4, 5, 7, 8, 0, 0], [0, 0, 3, 4, 5, 8, 8, 0, 0]]
[[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 7, 0], [0, 0, 3, 4, 5, 7, 8, 9, 0], [0, 0, 3, 4, 5, 8, 8, 11, 0]]
[[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 8, 8, 11, 12]]
"""
二维数组,先遍历背包

1.3 一维dp数组解决背包问题

通过滚动数组,把二维dp降为一维dp。

二维数组递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

如果把dp[i-1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i ][j], dp[i][j - weight[i]] + value[i]);

与其把dp[i-1]这一层拷贝到把dp[i],不如只用一个一维数组,只用dp[j](一维数组,可以理解成一个滚动数组即:重复利用上一层)。

1.3.1.确定dp数组以及下标含义

通过使用一维数组dp[j]:表示容量为j的背包,价值总和最大是多少。

1.3.2.确定递推公式

dp[j]可以通过dp[j-weight[i]] 推导出来。

有两个方向可以推出来递推公式

  1. 不放物品i:dp[j-weight[i]] 表示容量为 j-weight[i] 的背包所背的最大价值。
  2. 放物品i:dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。

所以,递推公式是求放物品和不放物品时的最大值,即:dp[[j] = max(dp[j],dp[j-weight[i]]+value[i])

1.3.3.dp数组初始化

初始化一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。

dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0] 就应该是0,

从递归公式:dp[j] = max(dp[j],dp[j-weight[i]]+value[i]) 可以看出来都初始为0即可。

1.3.4.确定遍历顺序

这里的遍历顺序及其讲究,先公布正确答案:1:先遍历物品,再遍历背包。2:背包倒序遍历。

def bag_1_wei_wp_first(bag_size, wp_info):
    dp = [0] * (bag_size + 1)  # 01 初始化dp数组,都为0
    # 02 更新数组:先遍历物品,再遍历背包
    for i in range(len(wp_info)):
        cur_item_weight = wp_info.get(i + 1).get("weight")  # 物品的id是从1 开始,所以等于当前i+1
        for j in range(len(dp) - 1, cur_item_weight - 1, -1):
            # 遍历背包容量,要倒序
            dp[j] = max(dp[j], dp[j - cur_item_weight] + wp_info.get(i + 1).get("value"))
        print(dp)
正确示例

问题1:为什么是倒序遍历背包?

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

倒序 背包重量
初始化 0 0 0 0 0 0 0 0 0
物品1 0 0 0 0 0 0 0 0 3
物品1 0 0 0 0 0 0 0 3 3
物品1 0 0 0 0 0 0 3 3 3
物品1 0 0 0 0 0 3 3 3 3
物品1 0 0 0 0 3 3 3 3 3
物品1 0 0 0 3 3 3 3 3 3
物品1 0 0 3 3 3 3 3 3 3
 
正序 背包重量
初始化 0 0 0 0 0 0 0 0 0
物品1 0 0 3 0 0 0 0 0 0
物品1 0 0 3 3 0 0 0 0 0
物品1 0 0 3 3 6 0 0 0 0
物品1 0 0 3 3 6 6 0 0 0
物品1 0 0 3 3 6 6 9 0 0
物品1 0 0 3 3 6 6 9 9 0
物品1 0 0 3 3 6 6 9 9 12

 

可以看到正序中首次出现问题的d[4]

从递归公式:dp[j] = max(dp[j],dp[j-weight[i]]+value[i]) 即:max(d[4],d[4-2]+3)。即 max(0,6)。物品1被重复放了两次,后面就开始重复放3次(9)、4次(12)。

问题2:为什么先遍历物品,能不能先遍历背包?

背包0
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
背包1
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
背包2
[0, 0, 3, 0, 0, 0, 0, 0, 0]
[0, 0, 3, 0, 0, 0, 0, 0, 0]
[0, 0, 3, 0, 0, 0, 0, 0, 0]
[0, 0, 3, 0, 0, 0, 0, 0, 0]
背包3
[0, 0, 3, 3, 0, 0, 0, 0, 0]
[0, 0, 3, 4, 0, 0, 0, 0, 0]
[0, 0, 3, 4, 0, 0, 0, 0, 0]
[0, 0, 3, 4, 0, 0, 0, 0, 0]
背包4
[0, 0, 3, 4, 6, 0, 0, 0, 0]
[0, 0, 3, 4, 6, 0, 0, 0, 0]
[0, 0, 3, 4, 6, 0, 0, 0, 0]
[0, 0, 3, 4, 6, 0, 0, 0, 0]
背包5
[0, 0, 3, 4, 6, 7, 0, 0, 0]
[0, 0, 3, 4, 6, 7, 0, 0, 0]
[0, 0, 3, 4, 6, 7, 0, 0, 0]
[0, 0, 3, 4, 6, 8, 0, 0, 0]
背包6
[0, 0, 3, 4, 6, 8, 9, 0, 0]
[0, 0, 3, 4, 6, 8, 9, 0, 0]
[0, 0, 3, 4, 6, 8, 9, 0, 0]
[0, 0, 3, 4, 6, 8, 9, 0, 0]
背包7
[0, 0, 3, 4, 6, 8, 9, 11, 0]
[0, 0, 3, 4, 6, 8, 9, 11, 0]
[0, 0, 3, 4, 6, 8, 9, 11, 0]
[0, 0, 3, 4, 6, 8, 9, 11, 0]
背包8
[0, 0, 3, 4, 6, 8, 9, 11, 12]
[0, 0, 3, 4, 6, 8, 9, 11, 12]
[0, 0, 3, 4, 6, 8, 9, 11, 12]
[0, 0, 3, 4, 6, 8, 9, 11, 12]
正序遍历背包会出现 累计求和问题
背包8
[0, 0, 0, 0, 0, 0, 0, 0, 3]
[0, 0, 0, 0, 0, 0, 0, 0, 4]
[0, 0, 0, 0, 0, 0, 0, 0, 5]
[0, 0, 0, 0, 0, 0, 0, 0, 8]
背包7
[0, 0, 0, 0, 0, 0, 0, 3, 8]
[0, 0, 0, 0, 0, 0, 0, 4, 8]
[0, 0, 0, 0, 0, 0, 0, 5, 8]
[0, 0, 0, 0, 0, 0, 0, 8, 8]
背包6
[0, 0, 0, 0, 0, 0, 3, 8, 8]
[0, 0, 0, 0, 0, 0, 4, 8, 8]
[0, 0, 0, 0, 0, 0, 5, 8, 8]
[0, 0, 0, 0, 0, 0, 8, 8, 8]
背包5
[0, 0, 0, 0, 0, 3, 8, 8, 8]
[0, 0, 0, 0, 0, 4, 8, 8, 8]
[0, 0, 0, 0, 0, 5, 8, 8, 8]
[0, 0, 0, 0, 0, 8, 8, 8, 8]
背包4
[0, 0, 0, 0, 3, 8, 8, 8, 8]
[0, 0, 0, 0, 4, 8, 8, 8, 8]
[0, 0, 0, 0, 5, 8, 8, 8, 8]
[0, 0, 0, 0, 5, 8, 8, 8, 8]
背包3
[0, 0, 0, 3, 5, 8, 8, 8, 8]
[0, 0, 0, 4, 5, 8, 8, 8, 8]
[0, 0, 0, 4, 5, 8, 8, 8, 8]
[0, 0, 0, 4, 5, 8, 8, 8, 8]
背包2
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
背包1
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
倒序遍历背包+遍历物品没有累计效果

1.3.5.代码示例

def bag_1_wei_wp_first(bag_size, wp_info):
    dp = [0] * (bag_size + 1)  # 01 初始化dp数组,都为0
    # 02 更新数组:先遍历物品,再遍历背包
    for i in range(len(wp_info)):
        print("物品" + str(i + 1))
        cur_item_weight = wp_info.get(i + 1).get("weight")  # 物品的id是从1 开始,所以等于当前i+1
        for j in range(len(dp) - 1, cur_item_weight - 1, -1):
            # 遍历背包容量,要倒序
            dp[j] = max(dp[j], dp[j - cur_item_weight] + wp_info.get(i + 1).get("value"))
            print(dp)


wp_info = {
    1: {"weight": 2, "value": 3},
    2: {"weight": 3, "value": 4},
    3: {"weight": 4, "value": 5},
    4: {"weight": 5, "value": 8},
}
bag_size = 8

bag_1_wei_wp_first(bag_size, wp_info)

"""
物品1
[0, 0, 0, 0, 0, 0, 0, 0, 3]
[0, 0, 0, 0, 0, 0, 0, 3, 3]
[0, 0, 0, 0, 0, 0, 3, 3, 3]
[0, 0, 0, 0, 0, 3, 3, 3, 3]
[0, 0, 0, 0, 3, 3, 3, 3, 3]
[0, 0, 0, 3, 3, 3, 3, 3, 3]
[0, 0, 3, 3, 3, 3, 3, 3, 3]
物品2
[0, 0, 3, 3, 3, 3, 3, 3, 7]
[0, 0, 3, 3, 3, 3, 3, 7, 7]
[0, 0, 3, 3, 3, 3, 7, 7, 7]
[0, 0, 3, 3, 3, 7, 7, 7, 7]
[0, 0, 3, 3, 4, 7, 7, 7, 7]
[0, 0, 3, 4, 4, 7, 7, 7, 7]
物品3
[0, 0, 3, 4, 4, 7, 7, 7, 9]
[0, 0, 3, 4, 4, 7, 7, 9, 9]
[0, 0, 3, 4, 4, 7, 8, 9, 9]
[0, 0, 3, 4, 4, 7, 8, 9, 9]
[0, 0, 3, 4, 5, 7, 8, 9, 9]
物品4
[0, 0, 3, 4, 5, 7, 8, 9, 12]
[0, 0, 3, 4, 5, 7, 8, 11, 12]
[0, 0, 3, 4, 5, 7, 8, 11, 12]
[0, 0, 3, 4, 5, 8, 8, 11, 12]
"""
正确示例(遍历顺序:先物品,再背包。背包倒序)
def bag_1_wei_wp_first_error(bag_size, wp_info):
    dp = [0] * (bag_size + 1)  # 01 初始化dp数组,都为0
    # 02 更新数组:先遍历物品,再遍历背包
    for i in range(len(wp_info)):
        print("物品" + str(i + 1))
        cur_item_weight = wp_info.get(i + 1).get("weight")  # 物品的id是从1 开始,所以等于当前i+1
        for j in range(cur_item_weight, len(dp)):
            # 遍历背包容量,正序遍历
            dp[j] = max(dp[j], dp[j - cur_item_weight] + wp_info.get(i + 1).get("value"))
            print(dp)


wp_info = {
    1: {"weight": 2, "value": 3},
    2: {"weight": 3, "value": 4},
    3: {"weight": 4, "value": 5},
    4: {"weight": 5, "value": 8},
}
bag_size = 8

bag_1_wei_wp_first_error(bag_size, wp_info)

"""
物品1
[0, 0, 3, 0, 0, 0, 0, 0, 0]
[0, 0, 3, 3, 0, 0, 0, 0, 0]
[0, 0, 3, 3, 6, 0, 0, 0, 0]
[0, 0, 3, 3, 6, 6, 0, 0, 0]
[0, 0, 3, 3, 6, 6, 9, 0, 0]
[0, 0, 3, 3, 6, 6, 9, 9, 0]
[0, 0, 3, 3, 6, 6, 9, 9, 12]
物品2
[0, 0, 3, 4, 6, 6, 9, 9, 12]
[0, 0, 3, 4, 6, 6, 9, 9, 12]
[0, 0, 3, 4, 6, 7, 9, 9, 12]
[0, 0, 3, 4, 6, 7, 9, 9, 12]
[0, 0, 3, 4, 6, 7, 9, 10, 12]
[0, 0, 3, 4, 6, 7, 9, 10, 12]
物品3
[0, 0, 3, 4, 6, 7, 9, 10, 12]
[0, 0, 3, 4, 6, 7, 9, 10, 12]
[0, 0, 3, 4, 6, 7, 9, 10, 12]
[0, 0, 3, 4, 6, 7, 9, 10, 12]
[0, 0, 3, 4, 6, 7, 9, 10, 12]
物品4
[0, 0, 3, 4, 6, 8, 9, 10, 12]
[0, 0, 3, 4, 6, 8, 9, 10, 12]
[0, 0, 3, 4, 6, 8, 9, 11, 12]
[0, 0, 3, 4, 6, 8, 9, 11, 12]
"""
错误示例(遍历顺序:先物品,再背包,背包正序)
def bag_1_wei_bag_first_asc(bag_size, wp_info):
    dp = [0] * (bag_size + 1)  # 01 初始化dp数组,都为0
    # 02 更新数组:先遍历背包,再遍历物品
    for j in range(len(dp)):
        # 背包正序
        print("背包" + str(j))
        for i in range(len(wp_info)):
            cur_item_weight = wp_info.get(i + 1).get("weight")  # 物品的id是从1 开始,所以等于当前i+1
            if cur_item_weight > j:
                dp[j] = max(dp[j], dp[j - 1])
            else:
                # 遍历背包容量,
                dp[j] = max(dp[j], dp[j - cur_item_weight] + wp_info.get(i + 1).get("value"))
            print(dp)


wp_info = {
    1: {"weight": 2, "value": 3},
    2: {"weight": 3, "value": 4},
    3: {"weight": 4, "value": 5},
    4: {"weight": 5, "value": 8},
}
bag_size = 8

bag_1_wei_bag_first_asc(bag_size, wp_info)

"""
背包0
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
背包1
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0]
背包2
[0, 0, 3, 0, 0, 0, 0, 0, 0]
[0, 0, 3, 0, 0, 0, 0, 0, 0]
[0, 0, 3, 0, 0, 0, 0, 0, 0]
[0, 0, 3, 0, 0, 0, 0, 0, 0]
背包3
[0, 0, 3, 3, 0, 0, 0, 0, 0]
[0, 0, 3, 4, 0, 0, 0, 0, 0]
[0, 0, 3, 4, 0, 0, 0, 0, 0]
[0, 0, 3, 4, 0, 0, 0, 0, 0]
背包4
[0, 0, 3, 4, 6, 0, 0, 0, 0]
[0, 0, 3, 4, 6, 0, 0, 0, 0]
[0, 0, 3, 4, 6, 0, 0, 0, 0]
[0, 0, 3, 4, 6, 0, 0, 0, 0]
背包5
[0, 0, 3, 4, 6, 7, 0, 0, 0]
[0, 0, 3, 4, 6, 7, 0, 0, 0]
[0, 0, 3, 4, 6, 7, 0, 0, 0]
[0, 0, 3, 4, 6, 8, 0, 0, 0]
背包6
[0, 0, 3, 4, 6, 8, 9, 0, 0]
[0, 0, 3, 4, 6, 8, 9, 0, 0]
[0, 0, 3, 4, 6, 8, 9, 0, 0]
[0, 0, 3, 4, 6, 8, 9, 0, 0]
背包7
[0, 0, 3, 4, 6, 8, 9, 11, 0]
[0, 0, 3, 4, 6, 8, 9, 11, 0]
[0, 0, 3, 4, 6, 8, 9, 11, 0]
[0, 0, 3, 4, 6, 8, 9, 11, 0]
背包8
[0, 0, 3, 4, 6, 8, 9, 11, 12]
[0, 0, 3, 4, 6, 8, 9, 11, 12]
[0, 0, 3, 4, 6, 8, 9, 11, 12]
[0, 0, 3, 4, 6, 8, 9, 11, 12]
"""
错误示例(遍历顺序:先背包,再物品,背包正序)
def bag_1_wei_bag_first_desc(bag_size, wp_info):
    dp = [0] * (bag_size + 1)  # 01 初始化dp数组,都为0
    # 02 更新数组:先遍历背包,再遍历物品
    for j in range(len(dp) - 1, 0, -1):
        print("背包" + str(j)) # 背包倒序
        for i in range(len(wp_info)):
            cur_item_weight = wp_info.get(i + 1).get("weight")  # 物品的id是从1 开始,所以等于当前i+1
            if cur_item_weight > j:
                dp[j] = max(dp[j], dp[j - 1])
            else:
                # 遍历背包容量,
                dp[j] = max(dp[j], dp[j - cur_item_weight] + wp_info.get(i + 1).get("value"))
            print(dp)


wp_info = {
    1: {"weight": 2, "value": 3},
    2: {"weight": 3, "value": 4},
    3: {"weight": 4, "value": 5},
    4: {"weight": 5, "value": 8},
}
bag_size = 8

bag_1_wei_bag_first_desc(bag_size, wp_info)

"""
背包8
[0, 0, 0, 0, 0, 0, 0, 0, 3]
[0, 0, 0, 0, 0, 0, 0, 0, 4]
[0, 0, 0, 0, 0, 0, 0, 0, 5]
[0, 0, 0, 0, 0, 0, 0, 0, 8]
背包7
[0, 0, 0, 0, 0, 0, 0, 3, 8]
[0, 0, 0, 0, 0, 0, 0, 4, 8]
[0, 0, 0, 0, 0, 0, 0, 5, 8]
[0, 0, 0, 0, 0, 0, 0, 8, 8]
背包6
[0, 0, 0, 0, 0, 0, 3, 8, 8]
[0, 0, 0, 0, 0, 0, 4, 8, 8]
[0, 0, 0, 0, 0, 0, 5, 8, 8]
[0, 0, 0, 0, 0, 0, 8, 8, 8]
背包5
[0, 0, 0, 0, 0, 3, 8, 8, 8]
[0, 0, 0, 0, 0, 4, 8, 8, 8]
[0, 0, 0, 0, 0, 5, 8, 8, 8]
[0, 0, 0, 0, 0, 8, 8, 8, 8]
背包4
[0, 0, 0, 0, 3, 8, 8, 8, 8]
[0, 0, 0, 0, 4, 8, 8, 8, 8]
[0, 0, 0, 0, 5, 8, 8, 8, 8]
[0, 0, 0, 0, 5, 8, 8, 8, 8]
背包3
[0, 0, 0, 3, 5, 8, 8, 8, 8]
[0, 0, 0, 4, 5, 8, 8, 8, 8]
[0, 0, 0, 4, 5, 8, 8, 8, 8]
[0, 0, 0, 4, 5, 8, 8, 8, 8]
背包2
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
背包1
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
[0, 0, 3, 4, 5, 8, 8, 8, 8]
"""
错误示例(遍历顺序:先背包,再物品,背包倒序)

 

二、0  1 背包 之 二维费用背包问题

1.1 问题描述

现在有4个物品,小偷背包支持最大重量为8,体积是20,怎么可以偷得价值最多的物品?(和一中数据一样,只是多了体积限制)

物品编号 重量体积价值
1 2 4 3
2 3 6 4
3 4 7 5
4 5 10 8

1.2 二维dp数组解决背包问题

1.2.1.确定dp数组以及下标含义

通过使用二维数组dp[v][w]:表示从下标为[0-w]的重量下,放入容量为v的背包,价值总和最大是多少。时刻要牢记这个dp数组的含义,

 背包重量 0背包重量 1背包重量 2背包重量 3背包重量 4背包重量 5背包重量 6背包重量 7背包重量 8
背包体积 0 0 0 0 0 0 0 0 0 0
背包体积 1 0  0  0  0  0  0  0  0
背包体积 2 0  0  0  0  0  0  0  0  0
背包体积 3 0  0  0  0  0  0  0  0  0
背包体积 4 0 0 3 3 3 3 3 3 3
背包体积 5 0 0 3 3 3 3 3 3 3
背包体积 6 0 0 3 4 4 4 4 4 4

 

1.2.2.确定递推公式

有两个方向可以推出来递推公式

  1. 物品不变:由dp[v][w]推出,即背包体积为v,里面最大价值就是 dp[v][w]。 
  2. 物品变化:由dp[v][w]推出,即背包体积为v,里面放入物品i,最大价值就是 dp[v-volum[i]][k-weight[i]]+value[i]。很好理解:把i占据的空间、重量去掉,把i的价值加上

所以,递推公式是求放物品和不放物品时的最大值,即:dp[v][w] = max(dp[v][w],  dp[v-volum[i]][k-weight[i]]+value[i])

1.2.3.dp数组初始化

上面的图表中已经初始化了背包重量为0和体积为0时的数据。ps:初始化一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。

从递归公式:dp[v][w] = max(dp[v][w],  dp[v-volum[i]][k-weight[i]]+value[i]) 可以看出来dp[v][w]是由左上方的数值推导出来的,空格中的数据其实是什么已不再重要,反正都会覆盖。

1.2.4.确定遍历顺序

问题来了,先遍历体积还是先遍历背包重量呢?其实都可以,最好是先遍历数据范围比较小的。

为什么都可以,其实很好理解。根据递推公式:取得是左上方数据。行列转换之后对其值无影响。

先遍历体积,其实就是按行慢慢填充;先遍历重量,其实就是按列慢慢填充。

1.2.5.代码示例

"""
01背包 - 二维费用
"""


def bag_2_wei_cost_first(bag_max_weight, bag_max_volume, wp_info):
    # 初始化数据
    dp = [[0] * (bag_max_weight + 1) for k in range(bag_max_volume + 1)]

    # 循环物品
    for item in wp_info:
        # 02 更新数组:先遍历重量,再遍历体积
        for w in range(bag_max_weight, 0, -1):
            if w < item.get("weight"):
                # 如果背包的重量 小于 物品的重量,那就不能好好玩耍了,由于是倒序排列也没必要continue,直接break
                break
            for v in range(bag_max_volume, 0, -1):
                if v < item.get("volume"):
                    # 如果背包的体积 小于 物品的体积,那也不能好好玩耍了,由于是倒序排列也没必要continue,直接break
                    break
                dp[v][w] = max(dp[v][w], dp[v - item.get("volume")][w - item.get("weight")] + item.get("value"))
        print(dp)


wp_info = [
    {"weight": 2, "volume": 4, "value": 3},
    {"weight": 3, "volume": 6, "value": 4},
    {"weight": 4, "volume": 7, "value": 5},
    {"weight": 5, "volume": 10, "value": 8},
]
bag_max_weight = 8
bag_max_volume = 20

bag_2_wei_cost_first(bag_max_weight, bag_max_volume, wp_info)
"""
[[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 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], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3]]
[[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 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], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7]]
[[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 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], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 7, 7, 7, 7], [0, 0, 3, 4, 5, 7, 8, 8, 8], [0, 0, 3, 4, 5, 7, 8, 8, 8], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9]]
[[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 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], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 8, 8, 8, 8], [0, 0, 3, 4, 5, 8, 8, 8, 8], [0, 0, 3, 4, 5, 8, 8, 8, 8], [0, 0, 3, 4, 5, 8, 8, 9, 9], [0, 0, 3, 4, 5, 8, 8, 11, 11], [0, 0, 3, 4, 5, 8, 8, 11, 11], [0, 0, 3, 4, 5, 8, 8, 11, 12], [0, 0, 3, 4, 5, 8, 8, 11, 12], [0, 0, 3, 4, 5, 8, 8, 11, 12], [0, 0, 3, 4, 5, 8, 8, 11, 12], [0, 0, 3, 4, 5, 8, 8, 11, 12]]
"""
二维费用

 

三、0  1 背包 之  分组背包

3:背包分析

3.1 问题描述

现在有4个物品,小偷背包最大容量为8,每类产品只能偷一种,怎么可以偷得价值最多的物品?

物品编号 重量价值种类
1 2 3 电子产品
2 3 4 电子产品
3 4 5 食品
4 5 8 食品

 

3.2 dp数组解决分组背包问题

3.2.1.确定二维dp数组以及下标含义

通过使用二维数组dp[i][j]:表示从下标为[0-i]的物品组中选择其中一件物品,放入重量为j的背包,价值总和最大是多少。时刻要牢记这个dp数组的含义,

物品组 重量0 重量1 重量2 重量3 重量4 重量5 重量6 重量7 重量8
(物品编号1,物品编号2) 0 0 3 4 4 4 4 4 4
(物品编号3,物品编号4) 0 0 3 4 5 8 8 9 12

3.2.2.确定一维dp数组以及下标含义

由于我们已经熟练的使用了数据降维为一维数组(滚动数组),我们可以直接推出一维数组的递推公式

有两个方向可以推出来递推公式

  1. 物品不变:由dp[j]推出,即背包重量为j,里面最大价值就是 dp[j]。 
  2. 放入物品:由dp[j]推出,里面放入物品i,最大价值就是 max(dp[j-物品A重量]+物品A价值,dp[j-物品B重量]+物品B价值,......)

所以,递推公式是求放物品和不放物品时的最大值,即:dp[[j] = max(dp[j], max(dp[j-物品A重量]+物品A价值,dp[j-物品B重量]+物品B价值,......)),简化之后去掉一层max

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

3.2.3.dp数组初始化

dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0] 就应该是0,

从递归公式:dp[j] = max(dp[j],dp[j-weight[i]]+value[i]) 可以看出来都初始为0即可。

3.2.4.确定遍历顺序

这里的遍历顺序及其讲究,1:先遍历物品组,再遍历背包。2:背包倒序遍历。3:再遍历背包组的物品。

3.2.5.代码示例

"""
01背包-分组背包
"""


def bag_1_wei_wp_first(bag_size, wp_info):
    dp = [0] * (bag_size + 1)  # 01 初始化dp数组,都为0
    # 02 更新数组:先遍历物品组,再(倒序)遍历背包,再遍历物品
    for wp_group in wp_info:
        # 遍历背包容量,要倒序
        for j in range(bag_size, 0, -1):
            # 再遍历背包中的物品, 
            for wp in wp_group:
                if j >  wp.get("weight"):
                    dp[j] = max(dp[j], dp[j - wp.get("weight")] + wp.get("value"))
            print(dp)


wp_info = [
    # 电子产品
    [{"weight": 2, "value": 3},
     {"weight": 3, "value": 4}],
    # 食品
    [{"weight": 4, "value": 5},
     {"weight": 8, "value": 8}]
]
bag_size = 8

bag_1_wei_wp_first(bag_size, wp_info)

""" 
[0, 0, 0, 0, 0, 0, 0, 0, 4]
[0, 0, 0, 0, 0, 0, 0, 4, 4]
[0, 0, 0, 0, 0, 0, 4, 4, 4]
[0, 0, 0, 0, 0, 4, 4, 4, 4]
[0, 0, 0, 0, 4, 4, 4, 4, 4]
[0, 0, 0, 3, 4, 4, 4, 4, 4]
[0, 0, 0, 3, 4, 4, 4, 4, 4]
[0, 0, 0, 3, 4, 4, 4, 4, 4]
[0, 0, 0, 3, 4, 4, 4, 4, 9]
[0, 0, 0, 3, 4, 4, 4, 8, 9]
[0, 0, 0, 3, 4, 4, 5, 8, 9]
[0, 0, 0, 3, 4, 5, 5, 8, 9]
[0, 0, 0, 3, 4, 5, 5, 8, 9]
[0, 0, 0, 3, 4, 5, 5, 8, 9]
[0, 0, 0, 3, 4, 5, 5, 8, 9]
[0, 0, 0, 3, 4, 5, 5, 8, 9]
"""
View Code

 

X、经典练习题

购物单

import sys
import copy

"""
分析:
    一、大方向
        典型的01背包问题,即:每个物品只能出现一次,在满足一定条件下,价值最大。
        满足花费、数量两重限制   (01背包之二维费用背包)
    二、差异点
        1:物品之间有主件和附件的内在联系,可以只买主件,不能只买附件 (打包成分组背包处理)
        2:价值计算方式:重要度*价格
方案:
    一、确定数据格式
        wp_info = {
            # 背包组1
            "1":[
                {"price":800,"level":2,"p_id":0,"value":1600}, # 只买主件
                {"price":1200=800+400,"level":5,"p_id":1,"value":3600=2000+1600}, # 主件和附件一起吗
            ]
        }
    二、使用二维数组
        dp = [[],[],[]]
    三、遍历方案
        先物品、
    四、确定递推公式
        dp[p][c] = max(dp[p][c],dp[p-price[i]][c-1]+value[i])
"""


def init_wp_info():
    bag_count = 0  # 定义背包中物品数量
    bag_price = 0  # 定义背包价格
    group_wp = {}
    wp_info = {}
    c_id = 0
    step = 100  # 定义价格步长,提升遍历速度
    for index, line in enumerate(sys.stdin):
        c_id = index
        list_line = line.strip().split(" ")
        if index == 0:
            bag_price = int(list_line[0])
            bag_count = int(list_line[1])
        else:
            wp_info[c_id] = {
                "price": int(list_line[0]),
                "count": int(1),
                "p_id": int(list_line[2]),
                "value": int(list_line[0]) * int(list_line[1]),
            }
            # 将主件创建背包组
            if int(list_line[2]) == 0:
                group_wp.setdefault(c_id, [wp_info[c_id]])
            if int(list_line[0]) % 100:
                step = 10
    # 打包=分组
    for k, _item in wp_info.items():
        group_id = _item.get("p_id")
        if group_id > 0:
            c_id += 1
            cur_group_wp = copy.deepcopy(group_wp.get(group_id))  # type:dict
            for item_y in cur_group_wp:
                # 把附件进行打包
                new_wp_info = {
                    "price": item_y.get("price") + _item.get("price"),
                    "count": item_y.get("count") + 1,
                    "p_id": item_y.get("p_id"),
                    "value": item_y.get("value") + _item.get("value"),
                }
                group_wp.get(group_id).append(new_wp_info)

    return group_wp, bag_count, int(bag_price), step


def get_max_value(group_wp, bag_count, bag_price, step):
    dp = [[0] * (bag_count + 1) for p in range(bag_price + 1)]  # ps:边界+1
    # 遍历物品组
    for k, wp_list in group_wp.items():
        for c in range(bag_count, 0, -1):  # 倒序遍历背包 之 背包物品容纳数量
            min_count = wp_list[0].get("count")
            if min_count > c:
                break
            for p in range(bag_price, 0, -step):  # 倒序遍历背包 之 背包物品价格,注意这个遍历的步长
                min_price = wp_list[0].get("price")
                if min_price > p:
                    break
                for ite in wp_list:
                    if ite.get("price") > p:
                        continue
                    dp[p][c] = max(
                        dp[p][c],
                        dp[p - ite.get("price")][c - ite.get("count")]
                        + ite.get("value"),
                    )
    return dp[-1][-1]


group_wp, bag_count, bag_price, step = init_wp_info()
result = get_max_value(group_wp, bag_count, bag_price, step)
print(result)
View Code

知识点/技巧:1:01背包、分组背包、二维费用背包

posted @ 2022-12-13 16:57  逍遥小天狼  阅读(69)  评论(0编辑  收藏  举报