1. 多重背包问题概述
多重背包问题是动态规划(DP)中的经典问题之一,它介于01背包和完全背包之间。在01背包中,每种物品只能选0次或1次;在完全背包中,每种物品可以选无限次;而多重背包则限制每种物品最多选s_i次。
这个问题在实际中有广泛应用,比如:
- 资源分配问题(有限的资金购买不同数量的多种商品)
- 投资组合优化(不同资产有购买上限)
- 物流装载(不同种类货物有数量限制)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多重背包的朴素解法
2.1 基本状态定义
多重背包的DP状态定义与01背包类似:
- dp[i][j]:考虑前i种物品,背包容量为j时的最大价值
- 状态转移方程:
dp[i][j] = max(dp[i-1][j-kv[i]] + kw[i])
其中0 ≤ k ≤ s_i且k*v[i] ≤ j
2.2 代码实现示例
python复制def multiple_knapsack(N, V, v, w, s):
dp = [[0]*(V+1) for _ in range(N+1)]
for i in range(1, N+1):
for j in range(V+1):
k = 0
while k <= s[i-1] and k*v[i-1] <= j:
dp[i][j] = max(dp[i][j], dp[i-1][j-k*v[i-1]] + k*w[i-1])
k += 1
return dp[N][V]
这种解法的时间复杂度是O(NVS),当S较大时效率很低。
3. 二进制优化方法
3.1 优化思路
二进制优化的核心思想是将多重背包转化为01背包。对于每种物品,我们不是把它看作s_i个相同的物品,而是通过二进制拆分将其组合成log(s_i)个"新物品"。
例如:某物品有13个,可以拆分为1,2,4,6(因为1+2+4+6=13),这样只需要4个新物品就能表示0-13的所有可能选择。
3.2 优化后的实现
python复制def binary_optimized(N, V, v, w, s):
# 二进制拆分
new_v = []
new_w = []
for i in range(N):
k = 1
while k <= s[i]:
new_v.append(v[i] * k)
new_w.append(w[i] * k)
s[i] -= k
k *= 2
if s[i] > 0:
new_v.append(v[i] * s[i])
new_w.append(w[i] * s[i])
# 01背包解法
dp = [0]*(V+1)
for i in range(len(new_v)):
for j in range(V, new_v[i]-1, -1):
dp[j] = max(dp[j], dp[j-new_v[i]] + new_w[i])
return dp[V]
这种优化将时间复杂度降为O(NVlogS),效率显著提升。
4. 单调队列优化
4.1 优化原理
单调队列优化可以进一步将复杂度降到O(N*V)。它利用了滑动窗口最大值的思想,对于每个余数类单独处理。
关键观察:对于容量j,它只能从j-v, j-2v,..., j-sv这些状态转移而来,这些状态对v的余数相同。
4.2 实现代码
python复制from collections import deque
def monotonic_queue(N, V, v, w, s):
dp = [0]*(V+1)
for i in range(N):
for r in range(v[i]):
q = deque()
for j in range(r, V+1, v[i]):
while q and j - q[0] > s[i]*v[i]:
q.popleft()
while q and dp[j] >= dp[q[-1]] + (j - q[-1])//v[i] * w[i]:
q.pop()
q.append(j)
dp[j] = max(dp[j], dp[q[0]] + (j - q[0])//v[i] * w[i])
return dp[V]
5. 牛客tracker上的常见问题
5.1 初始化陷阱
很多初学者会错误地初始化dp数组。正确的做法:
- 求最大价值:初始化为0
- 求恰好装满:dp[0]=0,其余初始化为-∞
5.2 空间优化技巧
可以像01背包一样优化到一维数组,但要注意:
- 二进制优化后直接使用01背包的一维写法
- 单调队列优化需要按余数类处理
5.3 常见变种
- 求方案数:将max改为sum
- 求具体方案:需要记录转移路径
- 混合背包:部分物品是01背包,部分是完全背包,部分是多重的
6. 实战案例分析
6.1 题目描述
牛客网一道经典多重背包题:
有N种硬币,第i种面值为v_i,有c_i枚,求凑出金额M的方案数。
6.2 解法实现
python复制def coin_change(N, M, v, c):
dp = [0]*(M+1)
dp[0] = 1
for i in range(N):
for j in range(M, -1, -1):
k = 1
while k <= c[i] and j >= k*v[i]:
dp[j] += dp[j - k*v[i]]
k += 1
return dp[M]
6.3 性能对比
测试数据:N=100, M=100000, v_i∈[1,100], c_i∈[1,1000]
- 朴素解法:超时
- 二进制优化:约200ms
- 单调队列优化:约50ms
7. 工程实践中的注意事项
- 物品数量很大时(如1e5),优先考虑单调队列优化
- 背包容量很大时(如1e9),可能需要考虑贪心或其他算法
- 注意数据范围,防止整数溢出
- Python中使用列表推导比普通循环更快
- 在竞赛中,通常需要自己实现队列而不是用库中的deque
8. 与其他背包问题的关系
8.1 与01背包的关系
当所有s_i=1时,多重背包退化为01背包。此时二进制优化相当于不做任何处理。
8.2 与完全背包的关系
当所有s_i≥V/v_i时,多重背包变为完全背包。此时可以用完全背包的优化方法。
8.3 与分组背包的关系
分组背包是多决策的背包问题,与多重背包的优化思路有相通之处。
9. 进阶学习建议
- 理解背包九讲中的数学证明
- 尝试用不同的语言实现(C++注重效率,Python注重简洁)
- 在LeetCode、牛客等平台刷相关题目
- 学习如何将实际问题抽象为背包模型
- 了解分支定界等更高级的优化方法
