完全背包# 多重背包
问题描述
有 n n n 个物品,每个物品有重量 w i w_i wi、价值 v i v_i vi 和数量限制 s i s_i si。现在有一个容量为 W W W 的背包,每个物品最多选择 s i s_i si 次,求解在不超过背包容量的情况下,能够装入物品的最大价值。
与其他背包的区别
- 01背包:每个物品最多选择1次
- 完全背包:每个物品可以选择无限次
- 多重背包:每个物品可以选择 [ 0 , s i ] [0,s_i] [0,si] 次
基本解法
状态定义
- d p [ i ] [ j ] dp[i][j] dp[i][j] 表示考虑前 i i i 个物品,背包容量为 j j j 时能获得的最大价值
状态转移方程
dp[i][j] = max(dp[i-1][j-k*w[i]] + k*v[i])
其中 k∈[0,s[i]] 且 k*w[i] ≤ j
二进制优化
将每个物品的数量 s i s_i si 进行二进制拆分,转化为01背包问题。
例如:数量为13可以拆分为1,2,4,6,因为13=1+2+4+6
C++ 实现(二进制优化)
int multipleKnapsack(vector<int>& w, vector<int>& v, vector<int>& s, int W) {
int n = w.size();
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
// 二进制拆分
int num = s[i];
for (int k = 1; num > 0; k <<= 1) {
int amount = min(k, num);
num -= amount;
// 01背包过程
for (int j = W; j >= w[i] * amount; j--) {
dp[j] = max(dp[j], dp[j - w[i] * amount] + v[i] * amount);
}
}
}
return dp[W];
}
Java 实现
class Solution {
public int multipleKnapsack(int[] w, int[] v, int[] s, int W) {
int n = w.length;
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++) {
int num = s[i];
for (int k = 1; num > 0; k <<= 1) {
int amount = Math.min(k, num);
num -= amount;
for (int j = W; j >= w[i] * amount; j--) {
dp[j] = Math.max(dp[j], dp[j - w[i] * amount] + v[i] * amount);
}
}
}
return dp[W];
}
}
Python 实现
def multipleKnapsack(w: List[int], v: List[int], s: List[int], W: int) -> int:
n = len(w)
dp = [0] * (W + 1)
for i in range(n):
num = s[i]
k = 1
while num > 0:
amount = min(k, num)
num -= amount
for j in range(W, w[i] * amount - 1, -1):
dp[j] = max(dp[j], dp[j - w[i] * amount] + v[i] * amount)
k <<= 1
return dp[W]
时间复杂度分析
- 朴素解法: O ( n ⋅ W ⋅ ∑ s i ) \mathcal{O}(n \cdot W \cdot \sum s_i) O(n⋅W⋅∑si)
- 二进制优化: O ( n ⋅ W ⋅ log max ( s i ) ) \mathcal{O}(n \cdot W \cdot \log \max(s_i)) O(n⋅W⋅logmax(si))
- 空间复杂度: O ( W ) \mathcal{O}(W) O(W)
优化方法
-
二进制拆分:
- 将每个物品拆分成二进制数量
- 显著减少状态转移的次数
-
单调队列优化:
- 对于较大的数量可以使用单调队列
- 时间复杂度可以优化到 O ( n ⋅ W ) \mathcal{O}(n \cdot W) O(n⋅W)
应用场景
- 库存管理问题
- 资源分配规划
- 生产计划制定
- 投资组合优化
- 商品促销策略
注意事项
- 数量限制的处理
- 二进制拆分的实现
- 整数溢出问题
- 内存使用限制
- 特殊情况处理
经典例题
问题描述
有 n n n 个物品,每个物品有重量 w i w_i wi 和价值 v i v_i vi。现在有一个容量为 W W W 的背包,每个物品可以选择任意次(无限次),求解在不超过背包容量的情况下,能够装入物品的最大价值。
与01背包的区别
- 01背包:每个物品最多选择一次
- 完全背包:每个物品可以选择无限次
动态规划解法
状态定义
- d p [ i ] [ j ] dp[i][j] dp[i][j] 表示考虑前 i i i 个物品,背包容量为 j j j 时能获得的最大价值
状态转移方程
dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i]) if j >= w[i]
dp[i][j] = dp[i-1][j] if j < w[i]
注意:与01背包的区别是 d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][j−w[i]] 而不是 d p [ i − 1 ] [ j − w [ i ] ] dp[i-1][j-w[i]] dp[i−1][j−w[i]]
代码实现
C++ 实现(二维)
int completeKnapsack(vector<int>& w, vector<int>& v, int W) {
int n = w.size();
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
if (j >= w[i-1]) {
dp[i][j] = max(dp[i-1][j], dp[i][j-w[i-1]] + v[i-1]);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][W];
}
C++ 实现(一维优化)
int completeKnapsack(vector<int>& w, vector<int>& v, int W) {
int n = w.size();
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
for (int j = w[i]; j <= W; j++) {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
return dp[W];
}
Java 实现
class Solution {
public int completeKnapsack(int[] w, int[] v, int W) {
int n = w.length;
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++) {
for (int j = w[i]; j <= W; j++) {
dp[j] = Math.max(dp[j], dp[j-w[i]] + v[i]);
}
}
return dp[W];
}
}
Python 实现
def completeKnapsack(w: List[int], v: List[int], W: int) -> int:
n = len(w)
dp = [0] * (W + 1)
for i in range(n):
for j in range(w[i], W + 1):
dp[j] = max(dp[j], dp[j-w[i]] + v[i])
return dp[W]
与01背包的代码区别
- 状态转移时使用当前行的状态
- 容量的遍历方向改为正序
- 可以直接从物品重量开始遍历
时间复杂度分析
- 时间复杂度: O ( n ⋅ W ) \mathcal{O}(n \cdot W) O(n⋅W)
- 空间复杂度:
- 二维实现: O ( n ⋅ W ) \mathcal{O}(n \cdot W) O(n⋅W)
- 一维实现: O ( W ) \mathcal{O}(W) O(W)
常见变形
- 求恰好装满背包的最大价值
- 求最少需要多少个物品装满背包
- 求装满背包的方案数
- 求满足条件的最小代价
应用场景
- 零钱兑换问题
- 物资补给规划
- 资源重复利用
- 生产计划制定
- 库存补货策略
注意事项
- 遍历顺序的区别
- 状态转移的依赖关系
- 初始化的处理
- 特殊情况的考虑
- 数据范围的限制