0 - 1 背包问题是一个经典的组合优化问题,属于动态规划算法的典型应用场景。该问题描述如下:
有一个容量为 C的背包,以及n个物品,每个物品有对应的重量 w i w_i wi和价值 v i ( i = 1 , 2... n ) v_i(i=1,2...n) vi(i=1,2...n)。对于每个物品,我们只有两种选择:要么将其放入背包,要么不放入,即 “0 - 1” 选择(选是 1,不选是 0)。目标是在不超过背包容量的前提下,选择一些物品放入背包,使得背包中物品的总价值最大。
通常使用二维数组 dp[i][j] 来表示状态,其中 i 表示考虑前 i 个物品,j 表示背包的当前容量,dp[i][j] 则表示在前 i 个物品中选择,背包容量为 j 时所能获得的最大价值。
对于第 i 个物品,有两种情况:
最终的最大价值存储在 dp[n][C] 中,其中 n 是物品的总数,C 是背包的容量。
def knapsack(weights, values, capacity):
n = len(weights)
# 创建二维数组 dp 并初始化
dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]
# 填充 dp 数组
for i in range(1, n + 1):
for j in range(1, capacity + 1):
if j < weights[i - 1]:
# 当前背包容量小于第 i 个物品的重量,不能放入
dp[i][j] = dp[i - 1][j]
else:
# 可以选择放入或不放入第 i 个物品,取最大值
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1])
return dp[n][capacity]
# 示例数据
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print(knapsack(weights, values, capacity))
优化思路
二维 dp 数组 dp[i][j] 表示考虑前 i 个物品,背包容量为 j 时的最大价值。在状态转移过程中,dp[i][j] 只依赖于 dp[i - 1][j] 和 dp[i - 1][j - w[i]],也就是说,当前行的状态只和上一行的状态有关。因此,我们可以使用一维数组 dp[j] 来表示背包容量为 j 时的最大价值,在更新 dp[j] 时,我们需要逆序遍历背包容量,这样可以保证更新 dp[j] 时使用的是上一轮(即考虑前 i - 1 个物品时)的状态。
def knapsack_optimized(weights, values, capacity):
n = len(weights)
# 初始化一维 dp 数组
dp = [0] * (capacity + 1)
# 遍历每个物品
for i in range(n):
# 逆序遍历背包容量
for j in range(capacity, weights[i] - 1, -1):
# 更新 dp[j] 的值
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[capacity]
# 示例数据
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print(knapsack_optimized(weights, values, capacity))
假设有一个小偷潜入了一家珠宝店,他带着一个容量为 8 千克的背包。店里有 4 件珠宝,每件珠宝有对应的重量和价值,如下表所示:
珠宝编号 | 重量(千克) | 价值(元) |
---|---|---|
1 | 2 | 3 |
2 | 3 | 4 |
3 | 4 | 5 |
4 | 5 | 6 |
小偷只能选择拿或者不拿某件珠宝,不能只拿一部分。他的目标是在不超过背包容量的情况下,拿到总价值最高的珠宝组合。
当考虑第一件珠宝(重量 2 千克,价值 3 元)时:
如果背包容量小于 2 千克,那么无法放入这件珠宝,最大价值和不考虑这件珠宝时一样。
如果背包容量大于等于 2 千克,就可以选择放入,此时要比较放入和不放入哪种情况能获得更大价值。
依次类推,考虑第二件、第三件、第四件珠宝,不断更新不同背包容量下的最大价值。最终,就能得出在背包容量为 8 千克时能拿到的珠宝的最大总价值。
完全背包问题是一个经典的动态规划问题,它是 0 - 1 背包问题的扩展。在 0 - 1 背包问题中,每种物品只有一个,我们只能选择放入或者不放入背包;而在完全背包问题里,每种物品有无限个,即每种物品可以选择放入背包 0 次、1 次、2 次…… 直到背包放不下为止。
问题描述为:有 n种物品,每种物品的重量为 w i w_i wi,价值为 v i ( i = 1 , 2..... n ) v_i(i=1,2.....n) vi(i=1,2.....n),背包的容量为C,每种物品可以使用任意多次,求在不超过背包容量的前提下,能获得的最大价值。
同样使用二维数组 d p [ i ] [ j ] dp[i][j] dp[i][j]来表示状态,其中 i 表示考虑前 i 种物品,j 表示背包的当前容量, d p [ i ] [ j ] dp[i][j] dp[i][j]表示在前 i 种物品中选择,背包容量为 j 时所能获得的最大价值。
对于第 i 种物品,有多种选择:可以不放入(即放入 0 次),也可以放入 1 次、2 次…… 直到背包放不下为止。所以状态转移方程可以表示为:
d p [ i ] [ j ] = max k = 0 ⌊ j / w [ i ] ⌋ ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) dp[i][j] = \max_{k = 0}^{\lfloor j / w[i] \rfloor}(dp[i - 1][j - k \times w[i]] + k \times v[i]) dp[i][j]=k=0max⌊j/w[i]⌋(dp[i−1][j−k×w[i]]+k×v[i])
不过,我们可以对这个方程进行优化,得到更简洁的形式。从另一个角度考虑, d p [ i ] [ j ] dp[i][j] dp[i][j] 的值要么是不放入第 i 种物品时的最大价值 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i−1][j],要么是放入至少一个第 i 种物品时的最大价值 d p [ i ] [ j − w [ i ] ] + v [ i ] dp[i][j - w[i]] + v[i] dp[i][j−w[i]]+v[i] 。因此优化后的状态转移方程为:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] if j < w [ i ] max ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w [ i ] ] + v [ i ] ) if j ≥ w [ i ] dp[i][j] = \begin{cases} dp[i - 1][j] & \text{if } j < w[i] \\ \max(dp[i - 1][j], dp[i][j - w[i]] + v[i]) & \text{if } j \geq w[i] \end{cases} dp[i][j]={dp[i−1][j]max(dp[i−1][j],dp[i][j−w[i]]+v[i])if j<w[i]if j≥w[i]
最终的最大价值存储在 d p [ i ] [ C ] dp[i][C] dp[i][C] 中,其中 n 是物品的种类数,C 是背包的容量。
def complete_knapsack(weights, values, capacity):
n = len(weights)
# 创建二维数组 dp 并初始化
dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]
# 填充 dp 数组
for i in range(1, n + 1):
for j in range(1, capacity + 1):
if j < weights[i - 1]:
# 当前背包容量小于第 i 种物品的重量,不能放入
dp[i][j] = dp[i - 1][j]
else:
# 可以选择放入或不放入第 i 种物品,取最大值
dp[i][j] = max(dp[i - 1][j], dp[i][j - weights[i - 1]] + values[i - 1])
return dp[n][capacity]
# 示例数据
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print(complete_knapsack(weights, values, capacity))
假设你要去超市购物,超市正在进行满减活动,满 200 元减 50 元。你有一张长长的购物清单,上面列了各种商品,每种商品都有对应的价格和你对它的喜爱程度(可以理解为价值),而且每种商品的库存是无限的。你想在凑满 200 元的前提下,尽可能地买到你喜欢的商品,也就是让商品的总价值最大,这就是一个完全背包问题。
商品的价格相当于物品的重量,商品的喜爱程度相当于物品的价值,满减的金额门槛相当于背包的容量,你需要在不超过这个金额门槛的情况下,选择合适的商品组合,使得总价值最大。
这是一个典型的完全背包问题的变种,我们可以使用动态规划来解决这个问题。动态规划的核心思想是将大问题分解为小问题,通过求解小问题的最优解来得到大问题的最优解。
def minCoins(arr, aim):
# 创建 dp 数组并初始化
dp = [float('inf')] * (aim + 1)
dp[0] = 0
# 遍历每种面值的货币
for coin in arr:
# 遍历所有可能的金额
for i in range(coin, aim + 1):
# 更新 dp[i] 的值
dp[i] = min(dp[i], dp[i - coin] + 1)
# 如果最终无法组成 aim,则返回 -1
if dp[aim] == float('inf'):
return -1
else:
return dp[aim]
# 测试示例
arr = [1, 2, 5]
aim = 11
print(minCoins(arr, aim))
多重背包问题也是一个经典的动态规划问题,它介于 0 - 1 背包问题和完全背包问题之间。在多重背包问题中,有 n种物品,每种物品有对应的重量 w i w_i wi、价值 v i v_i vi 以及数量上限 c i ( i = 1 , 2... n ) c_i(i=1,2...n) ci(i=1,2...n),背包的容量为C。我们需要在不超过背包容量的前提下,选择一些物品放入背包,使得背包中物品的总价值最大,且每种物品放入的数量不能超过其数量上限。
同上。。。。
对于第 i 种物品,我们可以选择放入 0 , 1 , . . . m i n ( c i , ∣ j / w i ∣ ) 0,1,...min(c_i,|j/w_i|) 0,1,...min(ci,∣j/wi∣) 个,状态转移方程为:
d p [ i ] [ j ] = max k = 0 min ( c i , ⌊ j / w i ⌋ ) ( d p [ i − 1 ] [ j − k × w i ] + k × v i ) dp[i][j] = \max_{k = 0}^{\min(c_i, \lfloor j / w_i \rfloor)}(dp[i - 1][j - k\times w_i] + k\times v_i) dp[i][j]=k=0maxmin(ci,⌊j/wi⌋)(dp[i−1][j−k×wi]+k×vi)
其中,k 表示第 i 种物品放入的数量, min ( c i , ⌊ j / w i ⌋ ) \min(c_i, \lfloor j / w_i \rfloor) min(ci,⌊j/wi⌋) 确保放入的数量既不超过该物品的数量上限 c i c_i ci ,也不超过当前背包容量所能容纳的该物品的最大数量。
同上。。。。
def multiple_knapsack(weights, values, counts, capacity):
n = len(weights)
# 创建二维数组 dp 并初始化
dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]
# 填充 dp 数组
for i in range(1, n + 1):
for j in range(1, capacity + 1):
# 不放入第 i 种物品
dp[i][j] = dp[i - 1][j]
# 尝试放入不同数量的第 i 种物品
for k in range(1, min(counts[i - 1], j // weights[i - 1]) + 1):
dp[i][j] = max(dp[i][j], dp[i - 1][j - k * weights[i - 1]] + k * values[i - 1])
return dp[n][capacity]
# 示例数据
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
counts = [2, 3, 1, 2]
capacity = 8
print(multiple_knapsack(weights, values, counts, capacity))
和 0 - 1 背包问题类似,多重背包问题也可以将二维的 dp 数组优化为一维数组,优化后的代码如下:
def multiple_knapsack_optimized(weights, values, counts, capacity):
n = len(weights)
# 初始化一维 dp 数组
dp = [0] * (capacity + 1)
# 遍历每个物品
for i in range(n):
# 逆序遍历背包容量
for j in range(capacity, 0, -1):
# 尝试放入不同数量的第 i 种物品
for k in range(1, min(counts[i], j // weights[i]) + 1):
dp[j] = max(dp[j], dp[j - k * weights[i]] + k * values[i])
return dp[capacity]
# 示例数据
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
counts = [2, 3, 1, 2]
capacity = 8
print(multiple_knapsack_optimized(weights, values, counts, capacity))
假设你是一位老师,要为班级采购文具,预算是 8 元。文具店有 4 种文具,每种文具有对应的价格、价值(比如对学生学习的帮助程度)和库存数量,如下表所示:
文具编号 | 价格(元) | 价值 | 库存数量 |
---|---|---|---|
1 | 2 | 3 | 2 |
2 | 3 | 4 | 3 |
3 | 4 | 5 | 1 |
4 | 5 | 6 | 2 |
你需要在不超过预算的情况下,选择合适的文具组合,使得采购的文具总价值最大,同时每种文具的采购数量不能超过其库存数量。这就是一个多重背包问题,其中预算相当于背包容量,文具的价格相当于物品的重量,文具的价值就是物品的价值,库存数量就是物品的数量上限。
分组背包问题是背包问题的一种变体。有 n 个物品,这些物品被划分成了 g 组,每组中的物品相互冲突,即每组中最多只能选择一个物品放入背包。每个物品有对应的重量 w i j w_ij wij和价值 v i j v_ij vij,其中 i 表示物品所在的组编号, j 表示该组内物品的编号,背包的容量为 C。目标是在不超过背包容量的前提下,选择合适的物品放入背包,使得背包中物品的总价值最大。
设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示考虑前 i 组物品,背包容量为 j 时所能获得的最大价值。
对于第 i 组物品,我们有两种选择:要么不选这一组的任何物品,此时 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i−1][j] ;要么从这一组中选一个物品 k 放入背包,前提是背包容量 j 要大于等于该物品的重量 w i k w_ik wik ,此时 d p [ i ] [ j ] = d p [ i − 1 ] [ j − w i k ] + v i k dp[i][j] = dp[i - 1][j - w_{ik}] + v_{ik} dp[i][j]=dp[i−1][j−wik]+vik 。
所以状态转移方程为:
d p [ i ] [ j ] = max ( d p [ i − 1 ] [ j ] , max k ∈ group i , j ≥ w i k ( d p [ i − 1 ] [ j − w i k ] + v i k ) ) dp[i][j]=\max\left(dp[i - 1][j],\max_{k\in\text{group}_i,j\geq w_{ik}}(dp[i - 1][j - w_{ik}]+v_{ik})\right) dp[i][j]=max(dp[i−1][j],k∈groupi,j≥wikmax(dp[i−1][j−wik]+vik))
其中 group i \text{group}_i groupi 表示第 i 组物品的集合。
最终的最大价值存储在 d p [ j ] [ C ] dp[j][C] dp[j][C] 中,其中 g 是物品的组数,C 是背包的容量。
def group_knapsack(groups, weights, values, capacity):
g = len(groups)
# 创建二维数组 dp 并初始化
dp = [[0 for _ in range(capacity + 1)] for _ in range(g + 1)]
# 遍历每组物品
for i in range(1, g + 1):
for j in range(1, capacity + 1):
# 不选第 i 组的任何物品
dp[i][j] = dp[i - 1][j]
group = groups[i - 1]
for k in group:
if j >= weights[k]:
dp[i][j] = max(dp[i][j], dp[i - 1][j - weights[k]] + values[k])
return dp[g][capacity]
# 示例数据
groups = [[0, 1], [2, 3]]
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print(group_knapsack(groups, weights, values, capacity))
同样可以将二维的 dp 数组优化为一维数组,优化后的代码如下:
def group_knapsack_optimized(groups, weights, values, capacity):
g = len(groups)
# 初始化一维 dp 数组
dp = [0] * (capacity + 1)
# 遍历每组物品
for i in range(g):
for j in range(capacity, -1, -1):
group = groups[i]
for k in group:
if j >= weights[k]:
dp[j] = max(dp[j], dp[j - weights[k]] + values[k])
return dp[capacity]
# 示例数据
groups = [[0, 1], [2, 3]]
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print(group_knapsack_optimized(groups, weights, values, capacity))
假设你计划去旅行,有不同类型的旅行套餐可供选择,每种类型的套餐只能选一个。比如有住宿套餐组、交通套餐组等。每个套餐有对应的价格(相当于物品重量)和旅行体验评分(相当于物品价值),而你旅行的预算是固定的(相当于背包容量)。你需要在预算范围内,从每个类型的套餐组中选择一个套餐,使得总的旅行体验评分最高,这就是一个分组背包问题。