一、问题描述(二维)
输入描述
你有一个背包,最多能容纳的体积是 V。
现在有 n 个物品,第 i 个物品的体积为 vi,价值为 wi。
(1)求这个背包至多能装多大价值的物品?
(2)若背包恰好装满,求至多能装多大价值的物品?
输入 :第一行两个整数 n 和 V,表示物品个数和背包体积。
接下来 n 行,每行两个数 vi 和 wi ,表示第 i 个物品的体积和价值
输出:第一行输出第一问的答案,第二行输出第二问的答案,如果无解输出0。
核心模型
- 0-1 背包问题:每件宝物只有一件 。对它,你只有两个选择:不拿(0) 或者 拿(1),
- 假设背包容量为 W。我们用一个二维表格 dp[i][j] 来记录状态,它代表的含义是:在前 i 件物品中进行选择,当背包容量还剩 j 时,能凑出的V价值最大价值
- 面对第 i 件物品(占容量为 w, 价值为 v)时,你需要做一道选择题,到了状态转移方程:
- 如果"有 3 个物品,背包最大容量(体积限制)为 5",那么初始化的二维 DP 表格大小应该是 4*6;需要留出专门的一行来表示一个物品都不选(0个物品)的初始状态
dp[i][j]表示"从前 i 个物品中挑选,总体积不超过 j 的所有选法中,能选出来的最大价值- 不选第 i 个:那么当前的最高价值完全取决于前 i-1 个物品。背包的容量也没有被占用,依然是 j
- 选第 i 个:背包必须装得下它,即当前容量 j 必须大于等于第 i 个物品的体积 v[i](满足 j - v[i] >= 0)
代码实现
javaimport java.util.Scanner; import java.util.Arrays; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); if (!sc.hasNextInt()) return; int n = sc.nextInt(); // 物品数量 int V = sc.nextInt(); // 背包最大体积 int[] v = new int[n + 1]; // 体积数组 int[] w = new int[n + 1]; // 价值数组 for (int i = 1; i <= n; i++) { v[i] = sc.nextInt(); w[i] = sc.nextInt(); } // ==================== 第一问:至多能装多大价值 ==================== // 定义二维数组:(n+1) 行, (V+1) 列 int[][] dp1 = new int[n + 1][V + 1]; // Java 默认初始化全为 0,即 dp1[0][j] 和 dp1[i][0] 自动为 0 for (int i = 1; i <= n; i++) { for (int j = 0; j <= V; j++) { // 情况 1:不选第 i 个物品,直接继承前 i-1 个物品在容量 j 时的价值 dp1[i][j] = dp1[i - 1][j]; // 情况 2:选第 i 个物品(前提是背包容量 j 装得下它) if (j >= v[i]) { dp1[i][j] = Math.max(dp1[i][j], dp1[i - 1][j - v[i]] + w[i]); } } } System.out.println(dp1[n][V]); // ==================== 第二问:若背包恰好装满(用 -1 表示无解) ==================== int[][] dp2 = new int[n + 1][V + 1]; // 初始化:全部填上 -1 for (int i = 0; i <= n; i++) { Arrays.fill(dp2[i], -1); } // 唯独背包容量为 0 时,什么都不装就是"恰好装满",价值为 0 // 注意:无论选到第几个物品,只要容量为 0,不选就是合法的装满状态 for (int i = 0; i <= n; i++) { dp2[i][0] = 0; } for (int i = 1; i <= n; i++) { for (int j = 1; j <= V; j++) { // 首先,不管能不能拿第 i 个物品,都可以选择"不拿" // 直接继承上一行数据(注意:如果上一行是 -1,这里也会继承 -1) dp2[i][j] = dp2[i - 1][j]; // 其次,如果背包塞得下物品 i,且上一行剩余容量的状态存在有效解(不为 -1) if (j >= v[i] && dp2[i - 1][j - v[i]] != -1) { // 如果刚才继承过来的是 -1(说明不拿第i个物品此容量无解),直接接受新组合 if (dp2[i][j] == -1) { dp2[i][j] = dp2[i - 1][j - v[i]] + w[i]; } else { // 如果原本就有解,则两者决出最大值 dp2[i][j] = Math.max(dp2[i][j], dp2[i - 1][j - v[i]] + w[i]); } } } } // 如果最后答案依然是 -1,说明无法恰好装满,按题目要求输出 0 if (dp2[n][V] == -1) { System.out.println(0); } else { System.out.println(dp2[n][V]); } sc.close(); } }物品数和体积**
+1,是为了给"边界状态"(也就是"什么都没有"的初始情况)留出专门的位置。** 如果数组不+1,代码在运行的第一步就会直接发生数组越界异常动态规划表格推导
输入: 3 个物品,背包总体积 V = 5。
物品信息:
物品 1:体积 2,价值 10
物品 2:体积 4,价值 5
物品 3:体积 1,价值 4
i (物品) \ j (容量) 0 1 2 3 4 5 变化含义解释 i = 0 (无物品) 0 0 0 0 0 0 没物品可选,价值全是 0。 i = 1 (v=2, w=10) 0 0 10 10 10 10 容量>= 2 时,能放下物品1,价值变 10。 i = 2 (v=4, w=5) 0 0 10 10 10 10 物品2性价比低,不如只拿物品1(保持不选的状态)。 i = 3 (v=1, w=4) 0 4 10 14 14 14 容量3时:拿物品3后剩容量2,去上一行查 dp[1][2]=10,得到 4+10=14。[第一问(至多装多大):初始化为 0]
dp[3][5]= 14(选第1、3个物品,总体积 3,没装满,但价值最大)
i \ j 0 1 2 3 4 5 变化含义解释 i = 0 0 -1 -1 -1 -1 -1 只有容量0能被"什么都不装"恰好塞满。 i = 1 (v=2, w=10) 0 -1 10 -1 -1 -1 容量2恰好被物品1装满,价值10。其他容量装不满。 i = 2 (v=4, w=5) 0 -1 10 -1 5 -1 容量4恰好被物品2装满,价值5。 i = 3 (v=1, w=4) 0 4 10 14 5 9 容量5时:拿物品3后剩容量4,上一行 dp[2][4]=5是合法的,所以 4+5=9(恰好装满!)。[第二问表格(恰好装满):初始化为 -1] 状态含义:
dp[i][j]表示从前i个物品中选,总体积恰好等于j的最大价值(-1 表示无法恰好装满)dp[3][5]= 9(选第2、3个物品,总体积 4+1=5,恰好装满)代码优化
①利用滚动数组做空间上的优化
实现第 i 行 j 列的时候,只需要判断第 i-1 行和 j 列,还有第 i-1 行和 j-vi 列,也就是只需要上一行 i-1 行的数据;这个时候,我们只需要一个2行 V+1列的数组
②再优化,使用一维数组,只需要一个1行 V+1列的数组
从左往右更新的时候会覆盖掉前面原来的值。所以我们要从右往左进行更新dp[i][j]
javaimport java.util.Scanner; import java.util.Arrays; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); if (!sc.hasNextInt()) return; int n = sc.nextInt(); // 物品数量 int V = sc.nextInt(); // 背包最大体积 int[] v = new int[n + 1]; // 体积数组 int[] w = new int[n + 1]; // 价值数组 for (int i = 1; i <= n; i++) { v[i] = sc.nextInt(); w[i] = sc.nextInt(); } // ==================== 第一问:至多能装多大价值 ==================== int[] dp1 = new int[V + 1]; // 默认初始化全为 0 for (int i = 1; i <= n; i++) { for (int j = V; j >= v[i]; j--) { dp1[j] = Math.max(dp1[j], dp1[j - v[i]] + w[i]); } } System.out.println(dp1[V]); // ==================== 第二问:若背包恰好装满(用 -1 表示无解) ==================== int[] dp2 = new int[V + 1]; // 初始化:全部填上 -1,代表目前所有容量都无法被恰好装满 Arrays.fill(dp2, -1); // 唯独容量为 0 的背包,什么都不装就是恰好装满,价值为 0 dp2[0] = 0; for (int i = 1; i <= n; i++) { for (int j = V; j >= v[i]; j--) { // 【核心修改点】:只有当剩余容量的状态不是 -1(即该容量可以被恰好装满)时,才能转移 if (dp2[j - v[i]] != -1) { // 如果当前 dp2[j] 也是 -1,说明之前没装满过,直接接受新组合 // 如果 dp2[j] 不是 -1,则跟新组合决出最大值 if (dp2[j] == -1) { dp2[j] = dp2[j - v[i]] + w[i]; } else { dp2[j] = Math.max(dp2[j], dp2[j - v[i]] + w[i]); } } } } // 如果最后答案依然是 -1,说明无法恰好装满,按题目要求输出 0 if (dp2[V] == -1) { System.out.println(0); } else { System.out.println(dp2[V]); } sc.close(); } }常见变体
背包类型 规则定义 生活场景类比 解题核心差异 0-1 背包 每种物品只有 1 件。 珠宝店盗宝(每件古董独一无二) 遍历容量时需要逆序(防止同一件物品被重复放入)。 完全背包 每种物品有 无限件。 超时货架购物(薯片管够,随便拿) 遍历容量时需要顺序(允许同一件物品重复叠加)。 多重背包 每种物品有 限定的数量 N_i 件。 菜市场买菜(土豆只有3个,西红柿只有5个) 可以拆解为 0-1 背包,或者用二进制拆分优化。 分数背包 物品可以 任意切分(拿走一部分)。 散装散称的黄金粉末、散装大米 极其简单,直接用贪心算法(优先挑性价比最高的切)。
二、类比背包问题--(三维背包问题)一和零
给你一个二进制字符串数组
strs和两个整数m和n。请你找出并返回
strs的最大子集的长度,该子集中 最多 有m个0和n个1。如果
x的所有元素也是y的元素,集合x是集合y的 子集 。链接:https://leetcode.cn/problems/ones-and-zeroes/description/
输入描述
示例 1
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 输出:4 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3示例 2
输入:strs = ["10", "0", "1"], m = 1, n = 1 输出:2 解释:最大的子集是 {"0", "1"} ,所以答案是 2 。物品 (i) :题目给出的字符串数组
strs中的每一个二进制字符串(如"10","0001")第一种代价 (v_i) :当前字符串中
0的个数 (对应背包问题里的体积);限制m个第二种代价 (m_i) :当前字符串中
1的个数 (对应背包问题里的重量);限制n个物品的价值 (w_i) :每选一个字符串,子集的长度+1;物品的价值固定为
1
状态转移方程初始化的三维 DP 表格大小应该是 (n+1) * (V+1)* (M+1)
表示从前 i 个物品中挑选,总体积不超过 j 、总重量不超过 k 的所有选法中,能选出来的最大价值
不选第 i 个:当前的最高价值完全取决于前 i-1 个物品。背包的体积和重量都没有被占用,依然是 j 和 k
选第 i 个 :背包必须同时装得下它的体积和重量!即当前体积 j >= v[i] 且当前重量 k >= m[i] ;拿走它,并去前 i-1 个物品里找扣除它体积和重量后的最大价值
代码实现
javaimport java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); // 1. 读取数组大小和限制条件 // 对应图 3 样例里的 strs 长度为 5, m = 5, n = 3 int len = sc.nextInt(); int m = sc.nextInt(); int n = sc.nextInt(); // 2. 读取所有的字符串物品 String[] strs = new String[len]; for (int i = 0; i < len; i++) { strs[i] = sc.next(); } // 3. 初始化动态规划数组 // 大小加 1 是因为容量可以为 0(代表 0 个 '0' 或 0 个 '1' 的边界状态) int[][] dp = new int[m + 1][n + 1]; // 4. 遍历每一个字符串 for (int i = 0; i < len; i++) { String str = strs[i]; // 统计当前字符串里 0 和 1 的个数 int zeros = 0; int ones = 0; for (int s = 0; s < str.length(); s++) { if (str.charAt(s) == '0') { zeros++; } else { ones++; } } // 5. 状态转移(核心部分):必须逆序从大到小遍历 for (int j = m; j >= zeros; j--) { for (int k = n; k >= ones; k--) { // 对比"不选"与"选择"当前字符串,哪个能装得更多 dp[j][k] = Math.max(dp[j][k], dp[j - zeros][k - ones] + 1); } } } // 6. 打印最终结果(对应图 3 的输出:4) System.out.println(dp[m][n]); sc.close(); } }






表示从前 i 个物品中挑选,总体积不超过 j 、总重量不超过 k 的所有选法中,能选出来的最大价值