资讯动态

动态规划中多重背包问题的单调队列优化解析

发布时间:2026/10/27 8:48:42 来源:尧图企业网站定制
1. 多重背包问题与朴素解法回顾多重背包问题是动态规划中的经典问题它介于01背包和完全背包之间。具体描述为给定N种物品和一个容量为V的背包第i种物品最多有s[i]件可用每件体积是w[i]价值是v[i]。如何选择装入背包的物品使得背包内物品的总价值最大1.1 多重背包的暴力解法最直观的解法是将多重背包转化为01背包问题处理。对于每种物品i我们将其视为s[i]个独立的物品然后使用01背包的解法def multiple_knapsack(N, V, w, v, s): dp [0] * (V 1) for i in range(N): for _ in range(s[i]): # 将第i种物品拆分为s[i]个01物品 for j in range(V, w[i] - 1, -1): dp[j] max(dp[j], dp[j - w[i]] v[i]) return dp[V]这种解法的时间复杂度为O(V*Σs[i])当s[i]较大时效率非常低。例如当每种物品都有1000件时算法需要进行1000N次内层循环。1.2 二进制优化方法为了优化暴力解法常用的方法是二进制拆分。将每种物品的件数s[i]拆分为1,2,4,...,2^k,s[i]-2^{k1}1的组合这样可以用log(s[i])个物品表示原来的s[i]件物品def binary_optimized(N, V, w, v, s): dp [0] * (V 1) for i in range(N): # 二进制拆分 k 1 remaining s[i] while k remaining: for j in range(V, k * w[i] - 1, -1): dp[j] max(dp[j], dp[j - k * w[i]] k * v[i]) remaining - k k * 2 if remaining 0: for j in range(V, remaining * w[i] - 1, -1): dp[j] max(dp[j], dp[j - remaining * w[i]] remaining * v[i]) return dp[V]这种方法将时间复杂度降为O(V*Σlog(s[i]))已经比朴素解法有了很大改进。但对于V和N都很大的情况如V1e5N1e3这种复杂度仍然不够理想。2. 单调队列优化原理剖析单调队列优化是多重背包问题的终极优化方案可以将时间复杂度降至O(NV)与01背包和完全背包的最优复杂度相同。这种优化的核心在于发现状态转移中的单调性并利用队列高效维护决策集合。2.1 状态转移方程的重新表述考虑多重背包问题的原始状态转移方程 dp[j] max(dp[j - kw[i]] kv[i]) for k in 0..s[i]我们可以按物品体积w[i]的余数将状态分类。令j u m*w[i]其中u j % w[i]m j // w[i]。这样对于固定的u我们可以将状态转移改写为dp[u m*w[i]] max(dp[u (m-k)w[i]] kv[i]) for k in 0..min(m, s[i])令k m - k可以进一步改写为 dp[u m*w[i]] max(dp[u k*w[i]] (m-k)*v[i]) for k in max(m-s[i], 0)..m提取与k无关的项 dp[u mw[i]] mv[i] max(dp[u k*w[i]] - k*v[i]) for k in max(m-s[i], 0)..m这样我们需要维护的就是一个关于k的滑动窗口最大值问题。2.2 单调队列的应用单调队列特别适合解决滑动窗口最大值问题。我们维护一个队列其中存储可能的决策k并保证队列中的元素按照dp[u k*w[i]] - k*v[i]单调递减。这样队首元素就是当前窗口的最大值。具体操作步骤对于每个余数u (0 ≤ u w[i])单独处理初始化空队列对于每个m (计算j u m*w[i] ≤ V)检查队首k是否超出窗口范围(m - k s[i])若超出则出队计算当前候选值val dp[u mw[i]] - mv[i]从队尾移除所有值小于val的元素保持队列单调性将m加入队尾更新dp[u mw[i]] queue[first] mv[i]这种方法的精妙之处在于每个k最多入队和出队各一次因此处理每种物品的时间复杂度是O(V)。3. 单调队列优化的实现细节3.1 基本实现框架基于上述原理我们可以实现单调队列优化的多重背包算法def monotonic_queue_optimized(N, V, w, v, s): dp [0] * (V 1) for i in range(N): wi, vi, si w[i], v[i], s[i] # 按余数u分类处理 for u in range(wi): # 初始化单调队列 queue [] # 计算最大的m使得u m*wi V max_m (V - u) // wi for m in range(max_m 1): current_j u m * wi # 计算当前候选值 candidate dp[current_j] - m * vi # 维护队列单调性 while queue and candidate dp[u queue[-1] * wi] - queue[-1] * vi: queue.pop() queue.append(m) # 移除超出窗口范围的元素 while queue[0] m - si: queue.pop(0) # 更新dp值 if queue: dp[current_j] max(dp[current_j], dp[u queue[0] * wi] (m - queue[0]) * vi) return dp[V]3.2 实现优化技巧在实际实现中我们可以进一步优化队列存储优化不需要在队列中存储完整的值只需存储k即可因为其他信息可以通过k计算得到。边界条件处理特别注意当w[i]0或s[i]0时的边界情况虽然在实际问题中这些情况通常不会出现。空间优化类似于01背包可以使用一维数组来优化空间复杂度。提前终止当处理某种物品时如果发现剩余容量已经无法装入任何一件该物品可以提前跳过后续处理。优化后的实现可能如下def optimized_mq_knapsack(N, V, w, v, s): dp [0] * (V 1) for i in range(N): wi, vi, si w[i], v[i], s[i] if wi 0: # 处理体积为0的物品 continue for u in range(wi): queue [] max_m (V - u) // wi for m in range(max_m 1): current_j u m * wi candidate dp[current_j] - m * vi # 维护单调队列 while queue and candidate dp[u queue[-1] * wi] - queue[-1] * vi: queue.pop() queue.append(m) # 移除过期元素 while queue[0] m - si: queue.pop(0) # 更新状态 if queue: dp[current_j] max(dp[current_j], dp[u queue[0] * wi] (m - queue[0]) * vi) return dp[V]4. 算法正确性证明与复杂度分析4.1 正确性证明单调队列优化多重背包的正确性基于以下两个关键点状态分类的正确性按余数u j % w[i]分类后不同余数类之间的状态转移完全独立不会相互影响。这保证了我们可以安全地分别处理每个余数类。单调队列维护最大值队列始终保持可能的决策点k按照dp[u k*w[i]] - k*v[i]单调递减且队首元素总是在当前窗口范围内。这保证了我们总能获取到窗口内的最大值。决策窗口的移动窗口[m-s[i], m]随着m的增加而向右滑动包含了所有可能的决策点不会遗漏任何可能的更优解。4.2 复杂度分析让我们分析单调队列优化后的时间复杂度外层循环遍历N种物品复杂度O(N)对于每种物品按余数u分类最多有w[i]类但所有物品的w[i]总和不超过V因为每种体积至少为1对于每个余数类使用单调队列处理每个m入队和出队最多各一次因此处理每个余数类的复杂度是O(V/w[i])综合起来总复杂度为O(NV)空间复杂度为O(V)因为我们只使用了一维dp数组。与二进制优化的O(VΣlog(s[i]))相比单调队列优化在s[i]较大时优势明显。例如当所有s[i]1000时二进制优化复杂度约为O(10NV)而单调队列优化严格保持O(NV)。5. 实际应用中的注意事项5.1 常见实现错误在实际编码实现单调队列优化时容易出现以下错误余数处理不当忘记按余数分类处理直接遍历所有容量这会破坏单调队列优化的前提条件。队列维护错误比较时使用了错误的值如直接比较dp值而忘记减去k*v[i]窗口范围计算错误如使用m - queue[0] s[i]而不是queue[0] m - s[i]初始化和边界条件忘记初始化dp数组没有处理w[i]0的特殊情况对m的循环范围计算错误5.2 调试技巧当实现出现问题时可以采用以下调试方法小数据测试构造小规模的测试用例手工计算预期结果与程序输出对比。打印中间结果在关键步骤打印队列状态和dp数组的变化观察是否符合预期。对比暴力解法对于中等规模的数据与暴力解法的结果对比验证正确性。分步验证先验证按余数分类是否正确再验证单调队列的维护是否正确最后验证状态转移是否正确5.3 性能优化建议虽然单调队列优化已经达到了理论最优复杂度但在实际应用中还可以考虑以下优化循环展开对于较小的w[i]可以手动展开内层循环减少循环开销。内存访问优化合理安排计算顺序提高缓存命中率。并行处理对于不同的余数类可以并行处理因为它们是相互独立的。语言级优化使用更高效的语言实现如C或者使用numpy等库进行向量化操作。6. 扩展与应用场景单调队列优化不仅适用于多重背包问题还可以应用于其他具有类似结构的动态规划问题。理解这种优化方法的本质可以帮助我们解决更多问题。6.1 其他适用问题滑动窗口最大值问题这是单调队列最直接的应用场景。有限制的序列问题如选择子序列要求某些条件限制且具有滑动窗口特性。特定类型的资源分配问题当资源分配具有类似背包问题的结构且决策具有滑动窗口特性时。6.2 算法竞赛中的应用在算法竞赛中单调队列优化是解决动态规划问题的利器。典型应用场景包括多重背包及其变种如分组背包、树形背包等变种问题。序列分割问题如将序列分成若干段每段满足某些条件求最优分割。带限制的最长子序列如求满足某些窗口限制条件的最长子序列。6.3 实际工程应用在实际工程中多重背包问题及其优化算法可以应用于资源分配如服务器资源分配、广告投放等场景。投资组合优化在有限的资金下选择最优的投资组合。生产计划合理安排生产批次最大化利润。7. 与其他优化方法的对比为了全面理解单调队列优化的优势我们将其与其他常见的多重背包优化方法进行对比。7.1 与二进制优化的对比特性二进制优化单调队列优化时间复杂度O(VΣlog(s[i]))O(NV)空间复杂度O(V)O(V)实现难度较简单较复杂适用场景s[i]较小任何情况常数因子较大较小扩展性较难扩展易于扩展从对比可以看出当s[i]较大时单调队列优化具有明显优势。但在s[i]都很小如s[i]≤10时二进制优化可能更简单实用。7.2 与混合背包的对比混合背包是指同时包含01背包、完全背包和多重背包物品的问题。对于这类问题二进制优化方法可以将所有物品统一转换为01背包物品处理实现简单但效率不高。单调队列优化需要对不同类型的物品采用不同的处理方式01背包物品直接按01背包处理完全背包物品按完全背包处理多重背包物品使用单调队列优化虽然单调队列优化的实现更复杂但在物品数量多、容量大时性能优势明显。7.3 性能实测对比为了直观展示不同优化方法的性能差异我们进行了一个简单的实验在普通笔记本电脑上使用Python实现测试用例N1000V100000所有w[i]随机在1-100v[i]随机在1-100s[i]随机在1-1000结果暴力解法无法在合理时间内完成二进制优化约15秒单调队列优化约3秒这个实验验证了单调队列优化的性能优势特别是在大规模数据情况下。8. 经典例题与实战练习为了巩固对单调队列优化多重背包的理解下面提供几个经典例题和实战练习建议。8.1 例题1基础多重背包问题描述 给定N种物品和容量V的背包。第i种物品体积w[i]价值v[i]数量s[i]。求最大价值。输入样例 N3, V10 w[2,3,4], v[3,4,5], s[3,2,2]解答 使用单调队列优化的代码可以直接解决这个问题。正确结果为13取两个第一种物品和两个第二种物品。8.2 例题2混合背包问题问题描述 背包问题中包含01物品、完全物品和多重物品。如何处理解决方案def hybrid_knapsack(N, V, w, v, s): dp [0] * (V 1) for i in range(N): if s[i] 1: # 01背包 for j in range(V, w[i] - 1, -1): dp[j] max(dp[j], dp[j - w[i]] v[i]) elif s[i] -1: # 完全背包假设用-1表示无限 for j in range(w[i], V 1): dp[j] max(dp[j], dp[j - w[i]] v[i]) else: # 多重背包 wi, vi, si w[i], v[i], s[i] for u in range(wi): queue [] max_m (V - u) // wi for m in range(max_m 1): current_j u m * wi candidate dp[current_j] - m * vi while queue and candidate dp[u queue[-1] * wi] - queue[-1] * vi: queue.pop() queue.append(m) while queue[0] m - si: queue.pop(0) if queue: dp[current_j] max(dp[current_j], dp[u queue[0] * wi] (m - queue[0]) * vi) return dp[V]8.3 实战练习建议实现验证从零开始实现单调队列优化的多重背包并用小数据测试正确性。性能对比实现二进制优化和单调队列优化版本用大规模数据测试性能差异。在线OJ练习POJ 1742 Coins多重背包可行性问题HDU 1059 Dividing多重背包判断能否平分Luogu P1776 宝物筛选标准多重背包问题变种挑战尝试解决分组背包问题尝试解决多维限制的多重背包问题尝试解决需要输出具体方案的多重背包问题9. 单调队列的其他应用场景虽然本文重点讨论单调队列在多重背包优化中的应用但单调队列作为一种重要的数据结构在其他许多问题中也有广泛应用。9.1 滑动窗口问题单调队列最经典的应用就是解决滑动窗口最大值/最小值问题。给定一个数组和窗口大小k求所有窗口中的最大值。解决方案def max_sliding_window(nums, k): from collections import deque q deque() result [] for i, num in enumerate(nums): while q and nums[q[-1]] num: q.pop() q.append(i) while q[0] i - k: q.popleft() if i k - 1: result.append(nums[q[0]]) return result9.2 动态规划优化除了多重背包单调队列还可以优化其他形式的动态规划问题特别是当状态转移方程可以表示为 dp[i] max(dp[j] cost(j,i)) for j in some range其中cost(j,i)满足某些单调性质时就可以考虑使用单调队列优化。9.3 图形问题在某些图形问题中如求矩形最大面积、直方图最大矩形等问题单调队列也能发挥作用。10. 进一步学习资源为了更深入地理解单调队列优化及其应用可以参考以下资源书籍《算法导论》动态规划章节《算法竞赛入门经典》背包问题章节《背包九讲》电子文档在线课程Coursera上的算法专项课程洛谷网校的动态规划专题课程开源代码GitHub上的各种算法实现库算法竞赛选手的模板代码论文关于动态规划优化的学术论文单调队列相关的研究论文在实际学习和应用中建议从简单问题入手逐步深入理解单调队列的工作原理和应用技巧。通过大量练习掌握如何识别可以使用单调队列优化的问题模式并熟练地实现优化方案。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑