01背包问题四阶解法:从暴力递归到空间优化动态规划(Hello 算法 Python 可视化逐行调试解析) 📅 发布时间:2026/9/8 23:27:13 👁 浏览次数: 01背包问题四阶解法从暴力递归到空间优化动态规划Hello 算法 Python 可视化逐行调试解析【免费下载链接】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 算法》仓库中俄文版的 0-1 背包问题可视化文件ru/codes/pythontutor/chapter_dynamic_programming/knapsack.md展开。该文件以 Python Tutor 可视化形式收录了同一问题的四个递进解法暴力递归knapsack_dfs、记忆化搜索knapsack_dfs_mem、二维动态规划knapsack_dp、空间优化一维动态规划knapsack_dp_comp。读完本文你将彻底理解 0-1 背包的状态定义与转移方程掌握「暴力 → 记忆化 → 动态规划 → 滚动数组」这一 DP 万能优化路径并能按仓库源码自行运行与可视化调试每个版本。一、问题定义与仓库中的示例数据0-1 背包问题给定n个物品第i个物品的重量为wgt[i-1]、价值为val[i-1]背包容量为cap。每个物品只能选择「装入」或「不装」一次求在不超过背包容量的前提下能装入的最大总价值。仓库在 ru/codes/python/chapter_dynamic_programming/knapsack.py 及各语言对应实现中统一使用如下示例数据wgt [10, 20, 30, 40, 50] # 物品重量 val [50, 120, 150, 210, 240] # 物品价值 cap 50 # 背包容量 n len(wgt) # 物品数量由上述数据可推得最优组合装入重量20与30的两个物品总重量恰好 50总价值120 150 270这也是四个版本统一输出的答案。该问题配套的完整推导、状态转移表动画与图解位于 docs/chapter_dynamic_programming/knapsack_problem.md俄文版位于 ru/docs/chapter_dynamic_programming/knapsack_problem.md属于「动态规划」章节中 0-1 背包问题一节的代码载体。说明目标可视化文件 ru/codes/pythontutor/chapter_dynamic_programming/knapsack.md 不存放直接可读的叙述性文字而是以四个 Python Tutor 深度链接的形式逐条承载对应函数的完整源码源码经 URL 编码内嵌于链接中并以标准注解行标注其来源!-- [file]{knapsack}-[class]{}-[func]{knapsack_dfs} -- !-- [file]{knapsack}-[class]{}-[func]{knapsack_dfs_mem} -- !-- [file]{knapsack}-[class]{}-[func]{knapsack_dp} -- !-- [file]{knapsack}-[class]{}-[func]{knapsack_dp_comp} --将链接参数解码还原后得到的内容与 ru/codes/python/chapter_dynamic_programming/knapsack.py 中四个函数的实现逐行一致即可视化文件与实际源码保持严格同步。运行该可视化后可沿调用栈逐行观察递归展开与剪枝、mem记忆表与dp表的逐格填充过程适合对照下面各节代码一起学习。二、第一步暴力搜索回溯思想knapsack_dfs0-1 背包可看作对每个物品依次做「不选 / 选」的二叉树决策。定义状态(i, c)从前i个物品中做选择剩余容量为c返回可获得的最大价值。那么不选物品i问题退化为(i-1, c)选物品i前提wgt[i-1] c得到价值val[i-1]剩余问题退化为(i-1, c - wgt[i-1])两者取max。def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) - int: 0-1 背包暴力搜索 # 若已选完所有物品或背包无剩余容量则返回价值 0 if i 0 or c 0: return 0 # 若超过背包容量只能选择不放入背包 if wgt[i - 1] c: return knapsack_dfs(wgt, val, i - 1, c) # 计算不放入和放入物品 i 的两种方案 no knapsack_dfs(wgt, val, i - 1, c) yes knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) val[i - 1] # 返回两种方案中价值更大的那一个 return max(no, yes)复杂度每个物品都有「选/不选」两个分支递归树深度为n时间复杂度为指数级O(2^n)空间上仅占用递归栈为O(n)。为什么要先写暴力版它直接对状态转移方程做了「朴素翻译」是后续一切优化的基准。在 Python Tutor 可视化中可以看到同一子问题如(i-1, c)在递归树的不同分支中被反复求解——这正是 0-1 背包存在「重叠子问题」的直接证据也是引入记忆化的动机。三、第二步记忆化搜索自顶向下knapsack_dfs_mem暴力搜索的重复计算可以通过一个(n1) × (cap1)的二维表mem消除。mem[i][c]记录状态(i, c)的最优解首次计算后写入后续命中直接返回。用-1表示尚未计算。def knapsack_dfs_mem( wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int ) - int: 0-1 背包记忆化搜索 # 若已选完所有物品或背包无剩余容量则返回价值 0 if i 0 or c 0: return 0 # 若已有记录则直接返回 if mem[i][c] ! -1: return mem[i][c] # 若超过背包容量只能选择不放入背包 if wgt[i - 1] c: return knapsack_dfs_mem(wgt, val, mem, i - 1, c) # 计算不放入和放入物品 i 的两种情况 no knapsack_dfs_mem(wgt, val, mem, i - 1, c) yes knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) val[i - 1] # 记录并返回两种方案中价值更大的那一个 mem[i][c] max(no, yes) return mem[i][c]调用入口处需要先初始化记忆表全表填充-1这一初始化逻辑在源码的Driver Code中体现mem [[-1] * (cap 1) for _ in range(n 1)] res knapsack_dfs_mem(wgt, val, mem, n, cap)复杂度每个状态(i, c)至多被真正计算一次共n × cap个状态故时间复杂度和空间复杂度均为O(n × cap)。相比指数级暴力搜索是质变其代价是引入了与 DP 表等大的记忆数组——这也是它和下一步「自底向上 DP」在时空指标上殊途同归的原因。可视化中mem[i][c] ! -1直接返回的分支即是「剪枝」路径可以直观看到命中次数远多于首次计算次数。四、第三步动态规划自底向上knapsack_dp将记忆化搜索的顺序翻转即为自底向上的填表法。定义dp[i][c]表示在容量c下对前i个物品决策所得的最大价值。初始化dp[0][*] dp[*][0] 0无物品可选或容量为 0 时价值为 0随后按物品序号和容量双重循环递推def knapsack_dp(wgt: list[int], val: list[int], cap: int) - int: 0-1 背包动态规划 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 - 1][c - wgt[i - 1]] val[i - 1]) return dp[n][cap]对转移方程的三点关键说明转移方向dp[i][c]只依赖上一行i-1的状态即要么是「同容量不取」的dp[i-1][c]要么是「腾出wgt[i-1]重量后装入」的dp[i-1][c - wgt[i-1]] val[i-1]容量越界分支当wgt[i-1] c时该物品物理上装不下只能继承dp[i-1][c]答案是dp[n][cap]对所有物品决策完、容量用尽上限的最优值。复杂度O(n × cap)时间与O(n × cap)空间二维表。相比记忆化搜索DP 消除了递归调用开销且天然无需担心递归深度。docs 中配套的「填表动画」图位于 docs/chapter_dynamic_programming/knapsack_problem.assets会逐格展示本示例数据下dp表如何从 0 增长到 270 的过程与可视化中的双重循环逐帧对应。五、第四步空间优化滚动数组knapsack_dp_comp观察转移方程可知计算第i行时只用到了第i-1行的数据因此二维表可压缩为一维数组dp[c]。但内层循环必须从大到小逆序遍历容量以保证dp[c - wgt[i-1]]在被读取时仍是「上一轮上一物品」的旧值而不是本轮刚被更新过的新值——这正是避免「一个物品被重复选择」的关键def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) - int: 0-1 背包空间优化后的动态规划 n len(wgt) # 初始化 dp 表 dp [0] * (cap 1) # 状态转移 for i in range(1, n 1): # 倒序遍历 for c in range(cap, 0, -1): if wgt[i - 1] c: # 若超过背包容量则不选物品 i dp[c] dp[c] else: # 不选和选物品 i 这两种方案的较大值 dp[c] max(dp[c], dp[c - wgt[i - 1]] val[i - 1]) return dp[cap]复杂度时间复杂度不变O(n × cap)空间复杂度由O(n × cap)降为O(cap)。为什么必须倒序遍历若正序遍历dp[c - wgt[i-1]]可能已经包含了「当前物品 i」的最优值等价于允许物品无限次取用那就变成了完全背包问题对应仓库中的 unbounded_knapsack.py。倒序保证了每次更新都基于上一轮结果从而维持「每个物品至多取一次」的 0-1 语义。这是 0-1 背包与完全背包在一维实现上的唯一分水岭也是面试与竞赛中的高频考点。对比提醒完全背包问题的一维写法恰恰需要正序遍历容量其可视化与实现见 codes/pythontutor/chapter_dynamic_programming/unbounded_knapsack.md阅读时可与本文对照加深对两种「背包范式」差异的理解。六、在仓库中运行与验证所有版本的入口代码都位于源文件的Driver Code中仓库统一以if __name__ __main__:组织测试例如俄文版源文件Driver Code if __name__ __main__: wgt [10, 20, 30, 40, 50] val [50, 120, 150, 210, 240] cap 50 n len(wgt) # 暴力搜索 res knapsack_dfs(wgt, val, n, cap) print(f不超过背包容量的最大物品价值 {res}) # 记忆化搜索 mem [[-1] * (cap 1) for _ in range(n 1)] res knapsack_dfs_mem(wgt, val, mem, n, cap) print(f不超过背包容量的最大物品价值 {res}) # 动态规划 res knapsack_dp(wgt, val, cap) print(f不超过背包容量的最大物品价值 {res}) # 空间优化后的动态规划 res knapsack_dp_comp(wgt, val, cap) print(f不超过背包容量的最大物品价值 {res})运行时可在仓库根目录依次执行两种方式任选其一# 运行俄文版源码 python3 ru/codes/python/chapter_dynamic_programming/knapsack.py # 运行简体中文版源码 python3 codes/python/chapter_dynamic_programming/knapsack.py四个版本依次调用后均会打印相同的最优价值270可用于互相验证实现正确性。若在本地 Python Tutor或任意支持逐步执行的环境中载入目标可视化文件中任意一段内嵌代码建议重点关注两个观察点暴力 → 记忆化统计递归调用次数可直观看到记忆表把指数级调用压缩到n × cap量级DP 二维 → 一维逐帧观察dp数组逆序覆盖的过程确认dp[c - wgt[i-1]]始终读的是旧值。七、四个版本速查与延伸阅读解法函数思想时间复杂度空间复杂度暴力搜索knapsack_dfs二叉决策树回溯O(2^n)O(n)栈记忆化搜索knapsack_dfs_mem自顶向下 备忘录memO(n × cap)O(n × cap)动态规划knapsack_dp自底向上二维填表O(n × cap)O(n × cap)空间优化 DPknapsack_dp_comp一维滚动数组 倒序O(n × cap)O(cap)仓库中该可视化文件与源码在简体中文codes/pythontutor/chapter_dynamic_programming/knapsack.md与日文等各语言目录均有同步副本且 Python 之外还提供 Java、C、C、Go、Rust、C#、JS、TS、Swift、Ruby、Kotlin、Dart 等多语言版本见各语言codes/*/chapter_dynamic_programming/knapsack.*便于横向对比同一算法的不同语言写法。进一步研究建议按如下顺序推进若想了解「为何 0-1 背包适合动态规划」的最优子结构与重叠子问题分析阅读 intro_to_dynamic_programming.md若想掌握通用 DP 解题流程问题拆解 → 状态定义 → 转移 → 优化阅读 dp_solution_pipeline.md若想挑战相似但转移相反的变体对比完全背包knapsack_problem.md 的延伸 相关章节以及 coin_change.md 等可视化教程。0-1 背包是动态规划章节的纲领性例题把「暴力递归 → 记忆化 → 自底向上 DP → 滚动数组」这条路径走通后面编辑距离、完全背包、零钱兑换等难题就都有了可复用的方法论脚手架。【免费下载链接】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),仅供参考