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

背包DP之0/1背包

背包DP之0/1背包

    • 一、0/1背包基本模型
      • 1.1 问题定义
      • 1.2 核心特征
    • 二、基础解法:二维DP
      • 2.1 状态设计与递推关系
      • 2.2 二维DP代码实现
      • 2.3 复杂度分析
    • 三、优化解法:一维DP(空间压缩)
      • 3.1 优化原理
      • 3.2 一维DP的关键:逆序遍历
      • 3.3 一维DP代码实现
        • 代码说明:
      • 3.4 复杂度分析
    • 四、0/1背包的变种问题
      • 4.1 变种1:恰好装满背包的最大价值
      • 4.2 变种2:计数问题(装满背包的方案数)
      • 4.3 变种3:二维约束背包(重量+体积)
    • 五、0/1背包的实际应用场景

0/1背包问题是动态规划(DP)领域的经典问题,也是理解“状态转移”和“空间优化”的绝佳案例,看似简单——给定物品和背包容量,选择物品装入背包使总价值最大(每个物品只能选一次),但其中蕴含的DP设计思想可推广到大量组合优化问题。

一、0/1背包基本模型

1.1 问题定义

给定n个物品,每个物品有两个属性:重量w[i]和价值v[i];有一个容量为C的背包。每个物品只能选择装入或不装入(0/1选择),求在背包不超过容量C的前提下,能装入物品的最大总价值。

示例:

  • 输入:n=3, C=4, w=[2,1,3], v=[4,2,3]
  • 输出:6(选择第0个和第1个物品,总重量2+1=3≤4,总价值4+2=6)

1.2 核心特征

  • 物品不可分割:每个物品要么全装,要么不装(区别于“完全背包”“多重背包”)。
  • 容量约束:装入物品的总重量不能超过背包容量。
  • 优化目标:最大化装入物品的总价值。

0/1背包的本质是带约束的组合优化问题,需在“选与不选”的决策中找到最优解,适合用动态规划求解。

二、基础解法:二维DP

2.1 状态设计与递推关系

动态规划的核心是“定义子问题并找到递推关系”。

  1. 定义状态
    dp[i][j]表示“考虑前i个物品(下标0~i-1),背包容量为j时的最大价值”。
    (注:i表示“物品数量”,j表示“当前背包容量”,子问题需同时约束这两个维度)

  2. 递推关系
    对于第i-1个物品(当前考虑的物品),有两种选择:

    • 不装入:总价值 = 前i-1个物品在容量j下的最大价值 → dp[i][j] = dp[i-1][j]
    • 装入(需满足j ≥ w[i-1]):总价值 = 前i-1个物品在容量j-w[i-1]下的价值 + 当前物品价值 → dp[i][j] = dp[i-1][j - w[i-1]] + v[i-1]
    • 取两种选择的最大值:
      dp[i][j] = max(dp[i-1][j], (j >= w[i-1] ? dp[i-1][j - w[i-1]] + v[i-1] : 0))
  3. 边界条件

    • i=0(无物品):dp[0][j] = 0(无论容量多大,价值都是0)。
    • j=0(容量为0):dp[i][0] = 0(无法装入任何物品)。

2.2 二维DP代码实现

public class ZeroOneKnapsack {public int maxValue(int n, int C, int[] w, int[] v) {// 二维DP数组:dp[i][j] = 前i个物品,容量j时的最大价值int[][] dp = new int[n + 1][C + 1];// 填充DP表:从1个物品开始遍历for (int i = 1; i <= n; i++) {// 当前物品的重量和价值(i-1对应原数组下标)int currW = w[i - 1];int currV = v[i - 1];// 遍历所有可能的容量for (int j = 1; j <= C; j++) {// 情况1:不装入当前物品int notTake = dp[i - 1][j];// 情况2:装入当前物品(需满足容量约束)int take = (j >= currW) ? (dp[i - 1][j - currW] + currV) : 0;dp[i][j] = Math.max(notTake, take);}}return dp[n][C]; // 考虑所有物品,容量为C时的最大价值}public static void main(String[] args) {ZeroOneKnapsack solution = new ZeroOneKnapsack();int n = 3; // 3个物品int C = 4; // 背包容量4int[] w = {2, 1, 3}; // 物品重量int[] v = {4, 2, 3}; // 物品价值System.out.println(solution.maxValue(n, C, w, v)); // 输出6}
}

2.3 复杂度分析

  • 时间复杂度O(n×C)O(n \times C)O(n×C)。外层循环遍历n个物品,内层循环遍历C种容量,总操作次数为n×Cn \times Cn×C
  • 空间复杂度O(n×C)O(n \times C)O(n×C)。二维DP数组需要存储(n+1) × (C+1)个状态。

适用场景:背包容量C较小(如C ≤ 1000)时,二维DP简单直观,易于理解和实现。

三、优化解法:一维DP(空间压缩)

二维DP的空间复杂度可优化至O(C)O(C)O(C),核心是利用“状态依赖关系”删除冗余维度。

3.1 优化原理

观察二维DP的递推关系:dp[i][j]仅依赖dp[i-1][j](上一行同列)和dp[i-1][j - w[i-1]](上一行左侧列)。这意味着:

  • 无需存储完整的二维数组,只需保留“上一行”的状态即可。
  • 可将二维数组压缩为一维数组dp[j],代表“当前行(第i个物品)容量j时的最大价值”。

3.2 一维DP的关键:逆序遍历

若直接复用一维数组并按正序遍历容量j,会导致“当前物品被多次装入”(违背0/1背包“每个物品只能选一次”的约束)。例如:

  • 正序遍历j时,计算dp[j]用到的dp[j - w[i-1]]可能已被更新(属于当前物品的状态),相当于多次装入。

解决方案逆序遍历容量j(从Cw[i-1])。

  • 逆序遍历确保计算dp[j]时,dp[j - w[i-1]]仍是“上一行”的状态(未被当前物品更新),符合0/1背包的约束。

3.3 一维DP代码实现

public class ZeroOneKnapsackOptimized {public int maxValue(int n, int C, int[] w, int[] v) {// 一维DP数组:dp[j] = 容量j时的最大价值(滚动更新)int[] dp = new int[C + 1];for (int i = 0; i < n; i++) { // 遍历每个物品int currW = w[i];int currV = v[i];// 逆序遍历容量(避免当前物品被多次选择)for (int j = C; j >= currW; j--) {// 不装入:dp[j](上一行状态);装入:dp[j - currW] + currVdp[j] = Math.max(dp[j], dp[j - currW] + currV);}}return dp[C];}public static void main(String[] args) {ZeroOneKnapsackOptimized solution = new ZeroOneKnapsackOptimized();int n = 3;int C = 4;int[] w = {2, 1, 3};int[] v = {4, 2, 3};System.out.println(solution.maxValue(n, C, w, v)); // 输出6}
}
代码说明:

以示例数据为例,一维dp数组的更新过程如下:

  1. 初始:dp = [0,0,0,0,0](容量0~4)
  2. 处理第0个物品(w=2, v=4):
    逆序遍历j=4→2
    • j=4dp[4] = max(0, dp[2]+4) = 4
    • j=3dp[3] = max(0, dp[1]+4) = 0(dp[1]=0)
    • j=2dp[2] = max(0, dp[0]+4) = 4
      此时dp = [0,0,4,0,4]
  3. 处理第1个物品(w=1, v=2):
    逆序遍历j=4→1
    • j=4max(4, dp[3]+2)=4(dp[3]=0)
    • j=3max(0, dp[2]+2)=6(dp[2]=4)
    • j=2max(4, dp[1]+2)=4
    • j=1max(0, dp[0]+2)=2
      此时dp = [0,2,4,6,4]
  4. 处理第2个物品(w=3, v=3):
    逆序遍历j=4→3
    • j=4max(4, dp[1]+3)=max(4,5)=5(dp[1]=2)
    • j=3max(6, dp[0]+3)=6
      最终dp = [0,2,4,6,5]dp[4]=5?不,示例输出应为6——注意:最终结果取dp[C],但此时dp[3]=6(容量3时价值6),而背包容量4允许装容量3的物品,因此最大价值为6。

3.4 复杂度分析

  • 时间复杂度O(n×C)O(n \times C)O(n×C)(与二维DP相同,未改变计算次数)。
  • 空间复杂度O(C)O(C)O(C)(从二维的O(n×C)O(n \times C)O(n×C)优化为一维的O(C)O(C)O(C))。

适用场景:背包容量C较大(如C ≤ 10^4)时,空间优化可显著减少内存占用,避免内存溢出。

四、0/1背包的变种问题

4.1 变种1:恰好装满背包的最大价值

问题:要求背包恰好装满,求最大价值(若无法装满,返回-1或其他标记)。

解法:

  • 初始化dp[j]-∞(表示无法装满),仅dp[0] = 0(容量0恰好装满,价值0)。
  • 递推时,仅当dp[j - currW]不是-∞(即j - currW可装满)时,才考虑装入当前物品。
public int maxValueExactlyFull(int n, int C, int[] w, int[] v) {int[] dp = new int[C + 1];Arrays.fill(dp, Integer.MIN_VALUE);dp[0] = 0; // 容量0恰好装满for (int i = 0; i < n; i++) {int currW = w[i];int currV = v[i];for (int j = C; j >= currW; j--) {if (dp[j - currW] != Integer.MIN_VALUE) {dp[j] = Math.max(dp[j], dp[j - currW] + currV);}}}return dp[C] == Integer.MIN_VALUE ? -1 : dp[C];
}

4.2 变种2:计数问题(装满背包的方案数)

问题:求恰好装满背包的方案总数(每个物品只能用一次)。

解法:

  • 初始化dp[0] = 1(容量0有1种方案:不装任何物品),其他dp[j] = 0
  • 递推时,dp[j] += dp[j - currW](装入当前物品的方案数 = 不装的方案数 + 装的方案数)。
public int countWays(int n, int C, int[] w) {int[] dp = new int[C + 1];dp[0] = 1; // 初始状态:容量0有1种方案for (int i = 0; i < n; i++) {int currW = w[i];for (int j = C; j >= currW; j--) {dp[j] += dp[j - currW];}}return dp[C];
}

4.3 变种3:二维约束背包(重量+体积)

问题:物品有重量w[i]和体积s[i]两个约束,背包有最大重量C和最大体积S,求最大价值。

解法:

  • 使用二维容量的DP数组dp[j][k]j为重量,k为体积)。
  • 递推时需同时满足重量和体积约束:dp[j][k] = max(dp[j][k], dp[j - w[i]][k - s[i]] + v[i])

五、0/1背包的实际应用场景

  1. 资源分配:有限预算下选择项目投资,最大化收益(预算=容量,项目成本=重量,项目收益=价值)。
  2. 任务选择:有限时间内选择任务,最大化任务总价值(时间=容量,任务耗时=重量,任务价值=价值)。
  3. 代码优化:作为其他DP问题的子结构(如“分割等和子集”“目标和”等问题可转化为0/1背包)。

总结与最佳实践
0/1背包是动态规划的入门经典,核心要点在于:

  • 状态设计:如何用“维度”刻画子问题(物品数+容量)。
  • 空间优化:利用状态依赖关系压缩空间(二维→一维,逆序遍历)。
  • 变种迁移:同一模型可解决不同场景的组合优化问题。

That’s all, thanks for reading~~
觉得有用就点个赞、收进收藏夹吧!关注我,获取更多干货~

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

相关文章:

  • 11-1 浅层神经网络及计算前向传播
  • 局部重要性注意力LIA,通过区域重要性图与门控机制实现高阶信息交互,自适应增强有用特征、抑制冗余信息,平衡模型性能与效率。
  • VR-Doh: 革新3D建模的虚拟现实体验
  • DPVR亮相青岛品牌日,崂山科创力量引领AI眼镜新浪潮
  • 基于PLC的轨检小车控制器设计
  • .NET-键控服务依赖注入
  • 【实战】Dify从0到100进阶--文档解读(13)API前端再开发
  • 苍穹外卖DAY11
  • 【LeetCode数据结构】栈和队列的应用——设计循环队列问题详解
  • 【后端】FastAPI的Pydantic 模型
  • Excel 将数据导入到SQLServer数据库
  • Java TCP 通信详解:从基础到实战,彻底掌握面向连接的网络编程
  • 通用表格识别技术的应用,深刻改变人们处理表格数据的方式
  • 如何最简单、通俗地理解Python的numpy库?
  • Ubuntu22.04.5 LTS安装与使用Docker
  • 【优选算法-多源 BFS】多源 BFS:解决多个起点的广度优先搜索
  • AI语境下创新教学模式应用示范与推广联盟成立| 南开大学携手和鲸,破解智能化时代教育难题
  • 只能在栈上创建对象
  • Linux网络-------1.socket编程基础---(UDP-socket)
  • 广州邮科万兆6光千兆48电工业级光纤交换机:三层功能如何重新定义网络智能化
  • Vue Scoped样式:当动态元素成为“无家可归“的孤儿
  • 2025年云南燃气经营企业从业人员考试题
  • Axios封装以及添加拦截器
  • UniApp X 网络请求避坑指南:从 JS 到 UTS 的 JSON 数据处理全解析
  • MCU驱动AD5231BRUZ_10K
  • GoLang学习笔记
  • Qt 菜单与工具栏设计:提升用户体验
  • stm32使用USB虚拟串口,因电脑缺少官方驱动而识别失败(全系列32单片机可用)
  • Git下载全攻略
  • 状压Dp和记忆化搜索