完全背包问题全面解析:状态转移推导、正序空间优化与零钱兑换双变体(Hello 算法)

发布时间:2026/9/10 4:31:25

完全背包问题全面解析:状态转移推导、正序空间优化与零钱兑换双变体(Hello 算法) 完全背包问题全面解析状态转移推导、正序空间优化与零钱兑换双变体Hello 算法【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo本文围绕《Hello 算法》完全背包问题一节对应俄语版 unbounded_knapsack_problem.md展开先厘清完全背包与 0-1 背包在物品可重复选取上的本质差异再推导二维dp状态转移方程、讲解一维空间优化为何必须正序遍历最后把同一套框架套用到零钱兑换最小硬币数与零钱兑换 II组合方案数两个典型变体上。读完你将掌握背包类 DP 的建模三步骤定状态、推转移、定边界能徒手区分反向遍历0-1与正向遍历完全背包的适用场景并可直接运行仓库内多语言代码验证结论。问题定义物品无限次的完全背包!!! question 给定 $n$ 个物品第 $i$ 个物品的重量为 $wgt[i-1]$、价值为 $val[i-1]$另有一个容量为 $cap$ 的背包。每个物品可以被选取任意多次求在不超过背包容量的前提下能装入的最大总价值。与 0-1 背包问题 相比唯一区别就是物品数量不受限。图 1 给出了书中配套的示例数据注意图中物品以索引 1 开头对应数组下标需减 1整个问题可以抽象为第 $i$ 个物品在决策时要么不拿要么拿一件。关键差异在于0-1 背包物品 $i$ 只有 1 件放入背包后只能继续从 $[i-1, c-wgt[i-1]]$ 状态递推即不再考虑它完全背包物品 $i$ 有无限件放入一件后仍然可以从前 $i$ 个物品中继续选择于是状态落到 $[i, c-wgt[i-1]]$。这一字之差正是推导全部差异的源头。状态定义与转移方程只改一处 i-1 → i沿用 0-1 背包的建模套路状态$[i, c]$表示从前 $i$ 个物品中选取、背包容积为 $c$ 时的最大价值记为 $dp[i, c]$决策对物品 $i$ 有两种选择状态的两种变化方式是不取物品 $i$容量不变转移到 $[i-1, c]$价值继承 $dp[i-1, c]$取一件物品 $i$容量消耗 $wgt[i-1]$、价值累加 $val[i-1]$转移到 $[i, c-wgt[i-1]]$价值为 $dp[i, c-wgt[i-1]] val[i-1]$。于是得到转移方程$$ dp[i, c] \max(dp[i-1, c], dp[i, c - wgt[i-1]] val[i-1]) $$边界条件没有物品$i0$或容量为 0$c0$时最大价值均为 0因此第一行与第一列初始化为 0若 $wgt[i-1] c$放不下只能执行不取分支。按 $i$、$c$ 递增的正序双层循环即可完成填表时间与空间复杂度均为 $O(n \times cap)$。仓库中的 Python 实现位于 unbounded_knapsack.pydef unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) - int: 完全背包动态规划 n len(wgt) # 初始化 dp 表 dp [[0] * (cap 1) for _ in range(n 1)] # 状态转移 for i in range(1, n 1): for c in range(1, cap 1): if wgt[i - 1] c: # 若超过背包容量则不选物品 i dp[i][c] dp[i - 1][c] else: # 不选和选物品 i 这两种方案的较大值 dp[i][c] max(dp[i - 1][c], dp[i][c - wgt[i - 1]] val[i - 1]) return dp[n][cap]代码文件内置了驱动用例wgt [1, 2, 3]、val [5, 11, 15]、cap 4每个物品重量 1/2/3、价值 5/11/15容量 4可推算最优解为取两件重量 2 的物品总价值 22。在仓库根目录直接运行验证python3 codes/python/chapter_dynamic_programming/unbounded_knapsack.py空间优化为什么完全背包要正序遍历与 0-1 背包一样二维dp可以压缩为一维数组。区别在于遍历方向恰好相反0-1 背包的状态依赖上一行的 $[i-1, c-wgt[i-1]]$。若一维数组从左到右更新左侧的 $dp[c-wgt[i-1]]$ 已在本轮被覆盖等效于把同一物品用了多次因此必须**从右到左倒序**遍历保证每件物品至多取一次完全背包需要允许再次取同一物品其转移依赖同一行左侧的 $dp[i, c-wgt[i-1]]$所以恰恰必须**从左到右正序**遍历——更新 $dp[c]$ 时读到的是本轮已经允许重复选取的状态从而自然实现无限件。这正是两个问题在压缩后代码上仅存的差别。对照参见同一章节的 knapsack_problem.md0-1 背包一节对倒序遍历的论证。压缩后的状态转移过程可参考书中图集unbounded_knapsack_dp_comp_step1.png至unbounded_knapsack_dp_comp_step6.png位于 unbounded_knapsack_problem.assets逐帧展示 $dp$ 一维数组如何被覆盖演进。实现只需去掉第一维并把内层循环改成for c in range(1, cap 1)正序遍历def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) - int: 完全背包空间优化后的动态规划 n len(wgt) # 初始化 dp 表 dp [0] * (cap 1) # 状态转移 for i in range(1, n 1): # 正序遍历 for c in range(1, cap 1): if wgt[i - 1] c: dp[c] dp[c] # 若超过背包容量则不选物品 i else: dp[c] max(dp[c], dp[c - wgt[i - 1]] val[i - 1]) return dp[cap]优化后空间复杂度由 $O(n \times cap)$ 降至 $O(cap)$时间复杂度仍为 $O(n \times cap)$。该dp_comp版本同样在同目录 unbounded_knapsack.py 内两种算法使用同一组用例并打印相同的最优价值。变体一零钱兑换求最少硬币数!!! question 给定 $n$ 种硬币第 $i$ 种面额为 $coins[i-1]$目标金额为 $amt$每种硬币可取无限枚。求凑出目标金额所需的最少硬币数若无法凑出返回 $-1$。图 2 给出了配套示例面额 1、2、5 三种硬币目标金额 11最优为1 5 5共 3 枚。与完全背包的映射关系零钱兑换是求最小化的完全背包特例对应关系与差异可整理为完全背包零钱兑换物品硬币物品重量 $wgt$硬币面额 $coins$背包容积 $cap$目标金额 $amt$最大化总价值最小化硬币数目标相反不超过容量即可必须恰好凑出金额精确匹配三步建模Step 1 · 定义状态子问题为用前 $i$ 种硬币恰好凑出金额 $a$ 所需的最少硬币数记作 $dp[i, a]$二维表尺寸为 $(n1) \times (amt1)$。Step 2 · 推导转移相比完全背包有两处变化——优化目标从max换为min每取一枚硬币数加 1而非加价值 $val$$$ dp[i, a] \min(dp[i-1, a], dp[i, a - coins[i-1]] 1) $$Step 3 · 确定边界$a 0$ 时不需要任何硬币因此整个首列 $dp[i, 0] 0$$i 0$没有硬币时凑不出任何正金额属于非法解应把整个首行 $dp[0, a]$ 置为 $\infty$让 $\min()$ 自动将其淘汰。用 amt1 哨兵规避整数溢出多数语言无法在int中表示 $\infty$若直接用int最大值转移中的1可能造成溢出。书中给出的工程化做法是用amt 1充当非法解标记——因为凑出金额 $amt$ 理论上最多也只需要 $amt$ 枚硬币面额为 1 时任何超过 $amt$ 的数值都必然代表无解。返回前检查 $dp[n, amt]$ 是否仍等于 $amt1$若是则返回 $-1$。参考实现见 coin_change.pyMAX amt 1贯穿始终空间优化版coin_change_dp_comp先整体填充MAX再单独令dp[0] 0def coin_change_dp_comp(coins: list[int], amt: int) - int: 零钱兑换空间优化后的动态规划 n len(coins) MAX amt 1 dp [MAX] * (amt 1) dp[0] 0 for i in range(1, n 1): # 正序遍历 for a in range(1, amt 1): if coins[i - 1] a: dp[a] dp[a] # 若超过目标金额则不选硬币 i else: dp[a] min(dp[a], dp[a - coins[i - 1]] 1) return dp[amt] if dp[amt] ! MAX else -1注意这里空间压缩后内层同样是正序遍历——这正是硬币可无限取的语义要求与完全背包一脉相承。填表过程可对照书中coin_change_dp_step1.png~coin_change_dp_step15.png的 15 帧图解同目录 assets 下。驱动用例为coins [1, 2, 5]、amt 4运行结果应返回 22 2python3 codes/python/chapter_dynamic_programming/coin_change.py变体二零钱兑换 II求组合方案数!!! question 给定 $n$ 种硬币面额为 $coins[i-1]$目标金额为 $amt$每种硬币可取无限枚。求凑出目标金额的不同硬币组合数。转移方程max/min 换成求和状态改为用前 $i$ 种硬币恰好凑出金额 $a$ 的组合方案数$dp$ 仍为 $(n1) \times (amt1)$。当前状态等于不取第 $i$ 种硬币与取一枚第 $i$ 种硬币两个分支的方案数之和$$ dp[i, a] dp[i-1, a] dp[i, a - coins[i-1]] $$边界条件也随之改变$a 0$不选任何硬币即可凑出 0是一种可行方案故整个首列 $dp[i, 0] 1$$i 0$没有硬币时凑不出任何正金额整个首行 $dp[0, a] 0$。实现见 coin_change_ii.py其驱动用例coins [1, 2, 5]、amt 5对应 4 种组合11111、1112、122、5返回dp[n][amt] 4。空间优化版仍只需去掉硬币种类维度并保持正序def coin_change_ii_dp_comp(coins: list[int], amt: int) - int: 零钱兑换 II空间优化后的动态规划 n len(coins) dp [0] * (amt 1) dp[0] 1 for i in range(1, n 1): # 正序遍历 for a in range(1, amt 1): if coins[i - 1] a: dp[a] dp[a] # 若超过目标金额则不选硬币 i else: dp[a] dp[a] dp[a - coins[i - 1]] return dp[amt]两个变体的时间复杂度均为 $O(n \times amt)$空间复杂度经压缩后为 $O(amt)$。三个问题一览与延伸阅读问题目标转移算子是否精确匹配非法/边界初始值完全背包价值最大$\max$否不超过容量首行首列为 0零钱兑换硬币数最少$\min$是恰好凑出首列 0、首行 $amt1$非法解零钱兑换 II组合数最多求和是恰好凑出首列 1、首行 0三个问题共享同一套物品无限次 → 转移依赖本轮左邻状态 → 一维压缩时正序遍历的核心规律差异只体现在目标算子max/min/求和与边界初值上。仓库在 chapter_dynamic_programming 目录下为每个问题都提供了配套的多语言实现与驱动测试例如Cunbounded_knapsack.c、coin_change.cJavacoin_change.java以Math.min与Arrays.fill(dp, MAX)展示哨兵初始化写法Go / C / Rust / TypeScript 等同名文件可对照查看不同语言如何表达∞哨兵与二维数组初始化关于 0-1 背包含为何倒序遍历的完整推导可继续阅读 knapsack_problem.md三个问题在 summary.md 与 dp_solution_pipeline.md 中还有更系统的归类总结。对照实验时只需修改各驱动用例的wgt/val/cap或coins/amt即可验证任意规模输入下的边界行为例如把零钱兑换中的硬币面额改为不含 1 的集合如[2, 5]、amt 1会触发dp[n][amt] MAX分支并返回 $-1$。【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/10 4:31:25

短剧AI视频工具选型指南:稳定性、一致性与工程化交付

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/10 5:41:32

CANN/ge GE图引擎设置符号形状API

EsSetOriginSymbolShape 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、Te…

2026/9/10 5:41:32

Arm-2D静态工程评测:嵌入式GUI落地前的关键可行性验证

1. 项目概述:为什么一个静态工程评测能决定嵌入式GUI项目的生死? Arm-2D 是 ARM 官方开源的、专为 Cortex-M 系列微控制器设计的轻量级 2D 图形加速库。它不是那种“跑个 demo 就完事”的玩具库,而是真正面向量产级嵌入式设备——比如智能手表…

2026/9/10 5:41:32

MicroPython轻量日志模块uLogLite设计与实战

1. 为什么 MicroPython 项目里,日志不能只是 print? 在 MicroPython 项目里,我见过太多人把 print("debug: x", x) 当成日志——直到某天设备在野外连续跑三天后突然卡死,串口连上去只看到一堆乱序的 "led on&q…

2026/9/10 5:36:31

RPA高级认证B卷:影刀与Alien RPA工程实战能力深度解析

简介:本资源为RPA高级认证最新B卷标准答案解析资料,面向正在备考RPA专业认证的技术人员、自动化工程师及企业流程优化从业者,旨在帮助考生精准把握考试重点、厘清高阶考点逻辑、提升应试策略与实操能力。压缩包共39.32MB,虽未提供…

2026/9/9 13:11:35

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/10 0:00:55

目录对比去重实战:用哈希算法精准清理重复文件

我电脑里现在还有一块换了三次机的“数据墓地”硬盘,里面存着2016年以前所有旧笔记本的完整备份。平时不觉得有什么,直到前阵子想把它整理归档,发现同一个安装包、同一批照片、同一份论文草稿,在几个不同的备份目录里反复出现。更…

2026/9/10 0:00:55

Leaflet离线地图完整Demo合集:内网部署与坐标纠偏实战

简介:这是一份面向Web GIS开发者的LeafLet离线地图示例合集,帮助开发者快速掌握离线地图从搭建到交互的完整流程。压缩包共723个文件,大小14.06MB,以319个js脚本、175个html页面和29个css样式文件为主体,配合png/svg图…

2026/9/10 0:00:55

MATLAB读取Rinex 3.02观测文件:多系统GNSS数据解析实战

简介:基于MATLAB开发的Rinex3.02版观测文件(o文件)读取代码包,面向卫星定位导航方向的学习者与研究人员,用于解决新版观测文件的数据解析、历元提取与时间转换问题。压缩包共4个文件,包含两个m脚本、一个19…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/9 10:21:54

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码