当前位置: 首页 > news >正文

动态规划中的背包问题:0/1 背包与完全背包的核心解析

动态规划(Dynamic Programming,简称 DP)是算法设计中一种重要的思想,其核心在于通过拆解问题、定义状态、寻找状态转移规律,利用子问题的解来高效求解复杂问题。而背包问题作为动态规划的经典应用场景,尤其是 0/1 背包和完全背包,常常是理解 DP 思想的最佳切入点。

一、动态规划的核心:状态与状态转移

在动态规划中,最关键的两个概念是状态状态转移

  • 状态:可以理解为问题在某一阶段的 “快照”,通常用一个数组(或矩阵)dp来表示。状态的定义需要精准覆盖问题的约束条件和求解目标。
  • 状态转移:指如何通过前一阶段的状态推导出当前阶段的状态。简单来说,就是 “用过去的结果决定现在的选择”。

对于背包问题,我们的核心目标是:在给定背包容量的限制下,从若干物品中选择组合,使总价值最大(或满足特定条件)。因此,dp数组的设计通常围绕两个核心维度:物品背包容量

二、0/1 背包:每个物品只能选一次

1. 问题特点

0/1 背包的关键约束是:每个物品要么被选入背包(1 次),要么不选(0 次),不能重复选择。

2. dp 数组的定义

通常定义dp[j]表示:背包容量为j时,能获得的最大价值(简化为一维数组,二维数组dp[i][j]表示前i个物品中选择时的状态,可优化为一维)。

初始化时,dp[0] = 0(容量为 0 时价值为 0),其余位置可初始化为 0 或负无穷(根据问题场景调整)。

3. 关键:遍历顺序的设计

0/1 背包的核心是确保每个物品只被使用一次,这完全依赖于遍历顺序的设计:

  • 外层循环遍历物品:依次考虑每个物品是否放入背包。
  • 内层循环倒序遍历容量:从最大容量max_weight向 0 遍历。

为什么要倒序?假设我们正处理第i个物品,倒序遍历容量j时,计算dp[j]所依赖的dp[j - weight[i]]是 “未处理第i个物品时的旧状态”(即不包含当前物品)。这样就能保证每个物品在当前容量下只被选择一次。

如果内层正序遍历会怎样?正序遍历会导致dp[j - weight[i]]可能已经包含了第i个物品(因为前面的容量已经更新过),从而导致同一物品被多次选择,违背 0/1 背包的约束。

4. Java 代码实现

public class ZeroOneKnapsack {public static int maxValue(int[] weights, int[] values, int capacity) {int n = weights.length;int[] dp = new int[capacity + 1];// 外层遍历物品for (int i = 0; i < n; i++) {// 内层倒序遍历容量(关键)for (int j = capacity; j >= weights[i]; j--) {// 状态转移方程:选或不选当前物品dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);}}return dp[capacity];}public static void main(String[] args) {int[] weights = {2, 3, 4, 5};  // 物品重量int[] values = {3, 4, 5, 6};   // 物品价值int capacity = 8;              // 背包容量System.out.println("0/1背包最大价值: " + maxValue(weights, values, capacity)); // 输出9}
}

三、完全背包:每个物品可以选多次

1. 问题特点

完全背包与 0/1 背包的唯一区别是:每个物品可以被无限次选择(只要背包容量允许)。

2. dp 数组的定义

与 0/1 背包一致,dp[j]仍表示容量为j时的最大价值。

3. 关键:遍历顺序的调整

完全背包允许物品重复选择,因此遍历顺序与 0/1 背包相反:

  • 外层循环遍历物品(与 0/1 背包一致)。
  • 内层循环正序遍历容量:从 0 向max_weight遍历。

为什么正序可行?正序遍历容量时,计算dp[j]所依赖的dp[j - weight[i]]可能已经包含了第i个物品(因为前面的容量已更新),这恰好符合 “物品可以重复选择” 的需求。例如,当j增大时,j - weight[i]可能已经放入过第i个物品,此时再放入一次,相当于多次选择该物品。

4. Java 代码实现

​
public class CompleteKnapsack {public static int maxValue(int[] weights, int[] values, int capacity) {int n = weights.length;int[] dp = new int[capacity + 1];// 外层遍历物品for (int i = 0; i < n; i++) {// 内层正序遍历容量(关键)for (int j = weights[i]; j <= capacity; j++) {// 状态转移方程:允许重复选择当前物品dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);}}return dp[capacity];}public static void main(String[] args) {int[] weights = {2, 3, 4, 5};  // 物品重量int[] values = {3, 4, 5, 6};   // 物品价值int capacity = 8;              // 背包容量System.out.println("完全背包最大价值: " + maxValue(weights, values, capacity)); // 输出12}
}​

四、内外层循环顺序的影响

除了遍历方向,物品和容量的内外层循环顺序也需要注意:

  • 0/1 背包:必须外层遍历物品,内层遍历容量(倒序)。若颠倒顺序(外层容量,内层物品),会导致同一物品在不同容量下被多次考虑,可能重复选择,违背 0/1 约束。

  • 完全背包:外层物品、内层容量(正序)是标准写法,但部分场景下颠倒顺序(外层容量、内层物品)也可实现(需结合具体问题验证)。

反例:0/1 背包错误的循环顺序

// 错误示例:0/1背包使用外层容量、内层物品的循环顺序
public static int wrongZeroOneKnapsack(int[] weights, int[] values, int capacity) {int n = weights.length;int[] dp = new int[capacity + 1];// 错误:外层遍历容量for (int j = 0; j <= capacity; j++) {// 错误:内层遍历物品for (int i = 0; i < n; i++) {if (j >= weights[i]) {dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);}}}return dp[capacity];
}

五、总结:0/1 背包与完全背包的核心区别

特性0/1 背包完全背包
物品选择限制每个物品最多选 1 次每个物品可选无限次
内层遍历方向倒序(从大到小)正序(从小到大)
状态依赖依赖 “未包含当前物品” 的旧状态依赖 “可能包含当前物品” 的新状态
外层循环必须遍历物品通常遍历物品(可调整)
代码核心差异for (j = capacity; j >= weight[i]; j--)for (j = weight[i]; j <= capacity; j++)

通过代码示例可以更直观地理解:遍历顺序的设计直接决定了状态依赖关系,这正是区分 0/1 背包和完全背包的本质所在。动态规划的灵活性在于,通过调整这些细节,就能适配不同的问题约束,而背包问题正是掌握这种思想的绝佳实践。

http://www.dtcms.com/a/393614.html

相关文章:

  • PHP应用-组件框架前端模版渲染三方插件富文本编辑器CVE审计(2024小迪安全DAY30笔记)
  • uniapp 如何判断发的请求是网络异常uni.request
  • 学习:uniapp全栈微信小程序vue3后台 (25)
  • 23种设计模式之【原型模式】-核心原理与 Java实践
  • Netty 重放解码器ReplayingDecoder揭秘:重写轻量异常机制 和 ConstantPool
  • getgeo 生物信息 R语言 表型信息表”“样本信息表”或“临床信息表 phenodata phenotype data
  • OceanBase备租户创建(二):通过BACKUP DATABASE PLUS ARCHIVELOG
  • Linux文件打包压缩与软件安装管理完全指南
  • KingbaseES数据备份操作详解(图文教程)
  • 中断屏蔽实现方法-ARM内核
  • Kotlin 协程之 SharedFlow 与 StateFlow 深度解析
  • python爬虫(请求+解析+案例)
  • 111-Christopher-Dall_Arm-Timers-and-Fire:Arm架构计时器与半虚拟化时间
  • switch缺少break出现bug
  • 【自然语言处理】(3) --RNN循环神经网络
  • C# 中的 ReferenceEquals 方法
  • BERT:用于语言理解的深度双向Transformer预训练【简单分析】
  • 力扣hot100:两数相加(模拟竖式加法详解)(2)
  • Zotero + Word 插件管理参考文献的引用
  • 用Python一键整理文件:自动分类DOCX与PDF,告别文件夹杂乱
  • Ubuntu部署Elasticsearch教程
  • 61.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--提取金额
  • 一款基于 .NET 开源、免费、命令行式的哔哩哔哩视频内容下载工具
  • Win Semi宣布推出线性优化的GaN工艺
  • 考研408计算机网络2025年第38题真题解析
  • C++编写的经典贪吃蛇游戏
  • 风险预测模型原理
  • PS练习5:利用翻转制作图像倒影
  • 平替Jenkins,推荐一款国产开源免费的CICD工具 - Arbess
  • aws 实战小bug