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

背包问题双雄:01 背包与完全背包详解(Java 实现)

一、背包问题概述

背包问题是动态规划领域的经典问题,其核心在于如何在有限容量的背包中选择物品,使得总价值最大化。根据物品选择规则的不同,主要分为两类:

  • 01 背包:每件物品最多选 1 次(选或不选)。
  • 完全背包:每件物品可选无限次。

本文将深入解析两者的核心逻辑、状态转移及优化技巧,并通过 Java 代码实现典型场景。

二、01 背包问题:选或不选的博弈

问题描述

给定背包容量 W 和 N 个物品(每个物品重量 w[i]、价值 v[i]),每个物品最多选 1 次,求背包能承载的最大价值。

核心思路

1. 状态定义
  • dp[j]:背包容量为 j 时能获得的最大价值。
2. 状态转移方程
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
  • 选第 i 件物品:需确保容量 j >= w[i],价值为前 i-1 件物品装入容量 j-w[i] 的背包价值 + 当前物品价值。
  • 不选第 i 件物品:价值与前 i-1 件物品装入容量 j 的背包价值相同。
3. 关键实现细节
  • 倒序遍历容量:从 W 到 w[i] 倒序枚举,避免重复选择同一物品。
  • 空间优化:使用一维数组代替二维数组,降低空间复杂度至 O(W)

示例分析

场景:背包容量 W=4,物品列表 [(w=2, v=3), (w=1, v=2), (w=3, v=4)]
二维 DP 表演变

容量 \ 物品0(无)物品 1 (2,3)物品 2 (1,2)物品 3 (3,4)
00000
10022
20333
30355
40355

一维优化 Java 代码

public class Knapsack01 {public static int solve(int[] w, int[] v, int W) {int N = w.length;int[] dp = new int[W + 1]; // dp[j]表示容量j的最大价值for (int i = 0; i < N; i++) { // 遍历每个物品for (int j = W; j >= w[i]; j--) { // 倒序遍历容量,避免重复选dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);}}return dp[W];}public static void main(String[] args) {int[] w = {2, 1, 3};int[] v = {3, 2, 4};int W = 4;System.out.println("01背包最大价值:" + solve(w, v, W)); // 输出:5}
}

三、完全背包问题:无限选择的智慧

问题描述

与 01 背包不同,完全背包允许每件物品选无限次,求背包能承载的最大价值。

核心思路

1. 状态定义

同 01 背包,dp[j] 表示容量为 j 时的最大价值。

2. 状态转移方程
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
  • 允许重复选择:由于每件物品可选多次,需正序遍历容量(从 w[i] 到 W),确保当前物品可被多次选取。
3. 关键实现细节
  • 正序遍历容量:从 w[i] 到 W 正序枚举,允许同一件物品被多次计算。

示例分析

场景:背包容量 W=4,物品列表同 01 背包示例(允许无限选)。
二维 DP 表演变

容量 \ 物品0(无)物品 1 (2,3)物品 2 (1,2)物品 3 (3,4)
00000
10022
20344
30366
40688

一维优化 Java 代码

public class KnapsackComplete {public static int solve(int[] w, int[] v, int W) {int N = w.length;int[] dp = new int[W + 1];for (int i = 0; i < N; i++) { // 遍历每个物品for (int j = w[i]; j <= W; j++) { // 正序遍历容量,允许重复选dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);}}return dp[W];}public static void main(String[] args) {int[] w = {2, 1, 3};int[] v = {3, 2, 4};int W = 4;System.out.println("完全背包最大价值:" + solve(w, v, W)); // 输出:8(选4件物品2)}
}

四、核心对比:01 背包 vs 完全背包

特性01 背包完全背包
物品选择每个物品最多选 1 次每个物品可选无限次
容量遍历顺序倒序(从 W 到 w [i])正序(从 w [i] 到 W)
状态更新逻辑基于 “旧状态” 避免重复基于 “新状态” 允许重复
时间复杂度O(N×W)O(N×W)
典型场景物品限购、资源分配货币兑换、原料无限供应

五、实战应用:LeetCode 经典题目

1. 01 背包应用:分割等和子集(LeetCode 416)

问题描述:判断数组是否可分割成两个和相等的子集。
思路:转化为 01 背包问题,目标容量为 total/2,判断是否能恰好装满。

public class CanPartition {public static boolean canPartition(int[] nums) {int total = sum(nums);if (total % 2 != 0) return false;int target = total / 2;boolean[] dp = new boolean[target + 1];dp[0] = true; // 容量0时有一种方案(不选任何物品)for (int num : nums) {for (int j = target; j >= num; j--) { // 倒序遍历防重复dp[j] = dp[j] || dp[j - num];}}return dp[target];}private static int sum(int[] nums) {return Arrays.stream(nums).sum();}public static void main(String[] args) {int[] nums = {1, 5, 11, 5};System.out.println(canPartition(nums)); // 输出:true(子集和为11)}
}

2. 完全背包应用:零钱兑换(LeetCode 322)

问题描述:用最少硬币数组成金额 amount,硬币可重复使用。
思路:转化为完全背包问题,目标是最小化物品数量(硬币数)。

public class CoinChange {public static int coinChange(int[] coins, int amount) {int[] dp = new int[amount + 1];Arrays.fill(dp, Integer.MAX_VALUE);dp[0] = 0; // 金额0时需0枚硬币for (int coin : coins) {for (int j = coin; j <= amount; j++) { // 正序遍历允许多选if (dp[j - coin] != Integer.MAX_VALUE) {dp[j] = Math.min(dp[j], dp[j - coin] + 1);}}}return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];}public static void main(String[] args) {int[] coins = {1, 2, 5};int amount = 11;System.out.println(coinChange(coins, amount)); // 输出:3(5+5+1)}
}

六、优化技巧与变种问题

1. 空间优化

  • 一维数组代替二维数组,空间复杂度从 O(N×W) 降至 O(W)

2. 多重背包转化

若物品有数量限制(如最多选 k 件),可通过二进制拆分转化为 01 背包问题。例如,最多选 3 件可拆分为 1 件、2 件两个物品。

3. 常见变种

  • 恰好装满背包的方案数:初始化 dp[0] = 1,其余为 0,通过加法原理计算方案数。
  • 二维费用背包:增加一维状态(如重量和体积),状态转移为 dp[j][k] = max(...)

七、总结与学习建议

核心口诀

  • 01 背包:倒序遍历防重复,选或不选取最值。
  • 完全背包:正序遍历允重复,同物多次算价值。

练习推荐

  • 01 背包:LeetCode 494. 目标和
  • 完全背包:LeetCode 518. 零钱兑换 II(求方案数,需正序遍历 + 组合逻辑)

通过对比学习 01 背包与完全背包的核心逻辑,掌握动态规划的状态转移思想,可有效应对背包问题的各类变种。建议结合具体题目反复练习,加深对 “状态定义” 和 “遍历顺序” 的理解。

相关文章:

  • React hook之useRef
  • 什么是Java bean的依赖注入
  • Vue3 PC端 UI组件库我更推荐Naive UI
  • Docker环境下FileRise私有云盘在飞牛NAS的部署与穿透实践
  • 《前端面试题:ES6新特性》
  • 行列视:企业数据分析新时代的利器(一)——深度解读与应用场景分析
  • 第2课 SiC MOSFET与 Si IGBT 静态特性对比
  • HarmonyOS运动开发:打造你的专属运动节拍器
  • Excel处理控件Aspose.Cells教程:在Excel 文件中创建、操作和渲染时间线
  • boost::filesystem::path文件路径使用详解和示例
  • Spring MVC执行流程简介
  • 玩转 Skia 的颜色
  • LeetCode - 543. 二叉树的直径
  • 如何开发ONLYOFFICE协作空间插件:完整教程
  • 大学生职业发展与就业创业指导教学评价
  • Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
  • std::ratio 简单使用举例
  • Cell的复用及自定义Cell
  • 【Zephyr 系列 16】构建 BLE + LoRa 协同通信系统:网关转发与混合调度实战
  • EasyImage实战:结合内网穿透技术实现私有图床部署过程
  • 北京网站建设 都选万维科技/软文价格
  • 邢台优化公司/湖南seo优化推荐
  • 做外贸怎样上外国网站/新闻类软文营销案例
  • 网站建设与维护岗位职责/最新做做网站
  • 论坛网站怎么做跳转/营销模式
  • 贵州网站备案局/国外免费网站服务器