第一题:卡码网46题(01背包问题 二维)
解题思路
本题考查的是利用 0-1 背包问题的思路,通过动态规划算法来解决在有限行李空间下选择研究材料以获取最大价值的问题,核心思路是基于对每个物品(研究材料)进行考虑,判断选或不选该物品时能达到的最大价值情况,以下是详细的说明:
-
确定使用动态规划及问题建模:
- 这是一个典型的 0-1 背包问题场景,因为每种研究材料只有选与不选两种选择,不能分割,要在给定的行李空间限制下,找出能获得最大价值的选择组合。符合动态规划通过子问题最优解来推导原问题最优解的特征,所以采用动态规划解决。
- 定义状态:创建一个二维数组
dp
,**其中dp[i][j]
表示在前i
种研究材料中(考虑顺序依次选择材料,从第 0 种到第i
种),当行李空间为j
时,所能获得的最大价值。**通过这样的二维数组,可以方便地对应不同的材料选择阶段以及不同的剩余行李空间情况,用于记录和推导最大价值。
-
输入数据读取与准备 :
通过
Scanner
类从标准输入读取相关数据。首先读取两个整数n
(研究材料的种类数)和bagweight
(行李空间大小),然后分别创建两个长度为n
的数组weight
(用于存储每种研究材料所占空间)和value
(用于存储每种研究材料的价值),并通过循环依次读取每种材料的空间和价值数据填充这两个数组,做好后续动态规划计算的数据准备。 -
初始化边界条件 :
对于
dp
数组的第一行(也就是只考虑第 0 种研究材料的情况),通过循环for (int j = weight[0]; j <= bagweight; j++)
进行初始化。当行李空间j
大于等于第 0 种研究材料的重量weight[0]
时,说明可以选择该材料,此时dp[0][j]
的最大价值就是该材料的价值value[0]
,因为只有这一种材料可供选择,只要空间允许就选它,所以将对应的dp[0][j]
赋值为value[0]
。 -
确定状态转移方程及循环计算 :
对于
i >= 1
(也就是考虑除了第一种材料之外的其他材料情况),通过两层嵌套的for
循环来遍历所有情况。外层循环控制考虑的材料种类(索引i
,循环条件是i < n
),内层循环控制行李空间大小(索引j
,循环条件是0 <= j <= bagweight
),在循环中:- 当行李空间
j
小于当前考虑的第i
种研究材料的重量weight[i]
时 :
意味着当前行李空间放不下第i
种材料,那这种情况下能获得的最大价值就和不考虑第i
种材料时(也就是在前i - 1
种材料中做选择,空间同样为j
的情况)的最大价值是一样的,所以dp[i][j] = dp[i - 1][j]
,直接继承之前的最大价值情况。 - 当行李空间
j
大于等于当前第i
种研究材料的重量weight[i]
时 :
此时有两种选择,一是不选第i
种材料,那么最大价值就是在前i - 1
种材料中,行李空间仍为j
时能获得的最大价值,即dp[i - 1][j]
;二是选择第i
种材料,那么最大价值就是选择了该材料后(价值增加了value[i]
),在剩余空间(j - weight[i]
)里在前i - 1
种材料中能获得的最大价值(也就是dp[i - 1][j - weight[i]]
)之和,我们需要取这两种情况中的较大值作为当前的最大价值,所以dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
,这就是本题的状态转移方程,通过不断地这样比较和更新,逐步推导出在考虑所有材料情况下,不同行李空间对应的最大价值。
- 当行李空间
-
返回最终结果 :
当两层嵌套循环结束后,
dp[n - 1][bagweight]
就表示在考虑了所有n
种研究材料后,当行李空间为bagweight
时所能获得的最大价值,将其输出就是本题所要求的答案,即小明能够携带的研究材料的最大价值。
代码
java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int bagweight = scanner.nextInt();
int[] weight = new int[n];
int[] value = new int[n];
for (int i = 0; i < n; ++i) {
weight[i] = scanner.nextInt();
}
for (int j = 0; j < n; ++j) {
value[j] = scanner.nextInt();
}
int[][] dp = new int[n][bagweight + 1];
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
for (int i = 1; i < n; i++) {
for (int j = 0; j <= bagweight; j++) {
if (j < weight[i]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
System.out.println(dp[n - 1][bagweight]);
}
}
第二题:卡码网46题(01背包问题 一维)
解题思路分析
本题是一个典型的 0-1 背包问题,旨在解决在有限的行李空间下,从多种研究材料中选择部分材料,使得携带的价值最大,每种材料只能选或不选,不能分割,整体采用动态规划的思路来解决,以下是详细说明:
-
确定使用动态规划及问题建模 :
这是 0-1 背包问题,因为对于每种研究材料只有两种抉择(选或者不选),并且要在给定行李空间限制下求最大价值组合,符合动态规划通过子问题最优解推导出原问题最优解的特性,所以用动态规划求解。
定义状态:这里使用一维数组
dp
来表示状态,其中dp[j]
表示在行李空间为j
时,能获得的最大价值。与二维数组表示状态的方式相比,这种一维优化的思路是利用了滚动数组的特性,通过合理的循环顺序来覆盖更新数据,实现同样的功能同时节省空间。 -
数据读取与准备 :
首先通过
Scanner
从标准输入读取相关数据。先读取M
(研究材料的种类数)和N
(行李空间大小),然后分别创建长度为M
的数组costs
(用于存储每种研究材料所占空间)和values
(用于存储每种研究材料的价值),并通过两个for
循环依次读取输入的每种材料的空间和价值数据,填充这两个数组,为后续动态规划计算准备好基础数据。 -
动态规划计算过程(核心逻辑):
- 外层循环遍历材料种类 :
通过for
循环(for (int i = 0; i < M; i++)
)从第一种材料开始,依次遍历到最后一种材料,每次循环代表考虑一种新的研究材料能否加入到选择中以增加最大价值。 - 内层循环更新不同空间下的最大价值(关键状态转移) :
对于每种材料i
,内层循环是for (int j = N; j >= costs[i]; j--)
,它从行李空间上限N
开始,逐步递减到当前材料i
的所占空间costs[i]
。这样倒序循环的原因是要保证每个dp[j]
的更新是基于上一轮(也就是还没考虑当前材料i
加入选择时)的状态值,避免重复选择和错误的数据覆盖,符合 0-1 背包问题一维优化的要求。
在循环中,根据状态转移方程dp[j] = Math.max(dp[j], dp[j - costs[i]] + values[i])
来更新dp[j]
的值,其含义是:对于当前的行李空间j
,有两种情况决定最大价值,一是不选择当前的第i
种研究材料,此时最大价值就是之前在空间j
下已经计算出的最大价值dp[j]
(也就是上一轮循环结束后的状态值 );二是选择当前的第i
种研究材料,那么价值就要加上该材料的价值values[i]
,同时行李空间要减去该材料所占空间costs[i]
,所以对应的最大价值就是dp[j - costs[i]] + values[i]
(dp[j - costs[i]]
是上一轮在剩余空间下能达到的最大价值),然后取这两种情况中的较大值作为新的dp[j]
,通过这样不断循环更新,逐步推导出在考虑完所有材料后,不同行李空间下所能获得的最大价值。
- 外层循环遍历材料种类 :
-
返回最终结果 :
当外层和内层循环都结束后,
dp[N]
就表示在给定的行李空间为N
时,通过前面的动态规划计算,所能携带的研究材料的最大价值,将其输出就是本题所要求的答案,即小明在有限行李空间下能够携带的研究材料的最大价值。
代码
java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取 M 和 N
int M = scanner.nextInt(); // 研究材料的数量
int N = scanner.nextInt(); // 行李空间的大小
int[] costs = new int[M]; // 每种材料的空间占用
int[] values = new int[M]; // 每种材料的价值
// 输入每种材料的空间占用
for (int i = 0; i < M; i++) {
costs[i] = scanner.nextInt();
}
// 输入每种材料的价值
for (int j = 0; j < M; j++) {
values[j] = scanner.nextInt();
}
// 创建一个动态规划数组 dp,初始值为 0
int[] dp = new int[N + 1];
// 外层循环遍历每个类型的研究材料
for (int i = 0; i < M; i++) {
// 内层循环从 N 空间逐渐减少到当前研究材料所占空间
for (int j = N; j >= costs[i]; j--) {
// 考虑当前研究材料选择和不选择的情况,选择最大值
dp[j] = Math.max(dp[j], dp[j - costs[i]] + values[i]);
}
}
// 输出 dp[N],即在给定 N 行李空间可以携带的研究材料的最大价值
System.out.println(dp[N]);
scanner.close();
}
}
第三题:416.分割等和子集
解题思路
1. 为什么可看成 01 背包问题
这道题可以看成 01 背包问题,原因如下:
我们把给定数组 nums
的所有元素总和看成是背包的 "容量",而要达成的目标是能否将这些元素分成两部分,使得两部分的和相等,也就是看能否从这些元素中挑选出一部分元素,它们的和恰好等于总和的一半(就好像往背包里装一些物品,让背包里装的物品价值总和刚好达到一个特定值一样)。在这里,数组中的每个元素就相当于 01 背包问题里的物品,每个元素都只有选(放入一个子集)或者不选(不放入这个子集)这两种情况,而且不能分割元素,这完全符合 01 背包问题的特征,即每个物品只能取 0 次或 1 次,所以可以用 01 背包的思路来解决本题。
2. 整体解题思路
本题的解题思路基于动态规划来实现,具体步骤如下:
-
计算数组元素总和并判断可行性 :
首先,通过循环遍历数组
nums
,累加所有元素得到总和sum
。如果这个总和是奇数,那显然无法将数组分割成两个元素和相等的子集,直接返回false
。若总和是偶数,则计算出目标值target
,也就是总和的一半,后续就是要尝试能否从数组元素中挑选出一些元素,使其和等于target
,这相当于在背包容量为target
的情况下装物品。 -
创建动态规划数组并初始化(隐式边界条件) :
创建一个长度为
target + 1
的一维数组dp
,其中dp[j]
表示在容量为j
的情况下(这里的容量类比于挑选元素累加和的大小),能得到的最大元素和。初始时数组元素默认都是 0,表示还没有开始挑选元素时,任何容量下能达到的和都是 0,这也相当于一种边界情况的初始化。 -
动态规划核心计算(两层循环):
- 外层循环 :通过
for
循环遍历数组nums
的每个元素,相当于依次考虑每一个 "物品" 是否要放入 "背包"。每次循环针对当前的元素nums[i]
去尝试更新不同 "容量"(也就是不同累加和情况)下能达到的最大元素和。 - 内层循环 :内层循环从目标容量
target
开始,倒序遍历到当前元素nums[i]
的大小。倒序的原因和普通 01 背包问题中内层循环倒序一样,是为了保证每个dp[j]
的更新是基于上一轮(也就是还没考虑当前元素nums[i]
加入选择时)的状态值,避免重复选择和错误的数据覆盖,符合 01 背包问题一维优化的要求。在循环中,根据状态转移方程dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i])
来更新dp[j]
的值,其含义是:对于当前的 "容量"j
,有两种情况决定最大元素和,一是不选择当前的第i
个元素,此时最大元素和就是之前在 "容量"j
下已经计算出的最大元素和dp[j]
(也就是上一轮循环结束后的状态值);二是选择当前的第i
个元素,那么元素和就要加上该元素的值nums[i]
,同时 "容量" 要减去该元素所占的 "空间"(也就是nums[i]
),所以对应的最大元素和就是dp[j - nums[i]] + nums[i]
(dp[j - nums[i]]
是上一轮在剩余 "容量" 下能达到的最大元素和),然后取这两种情况中的较大值作为新的dp[j]
,通过这样不断循环更新,逐步推导出在考虑完所有元素后,不同 "容量" 下所能获得的最大元素和。
- 外层循环 :通过
-
判断结果并返回 :
在每次外层循环结束(也就是每考虑完一个元素后),都检查一下
dp[target]
是否已经等于target
了,如果等于,那就说明已经找到了一组元素,它们的和恰好等于目标值,也就是可以将数组分割成两个子集满足条件,直接返回true
。如果整个外层循环结束后都没有出现这种情况,那就最后再检查一次dp[target]
是否等于target
,并返回相应的布尔值,表示是否能分割数组成满足要求的两个子集。
代码
java
class Solution {
public boolean canPartition(int[] nums) {
if(nums == null || nums.length == 0)return false;
int n = nums.length;
int sum = 0;
for(int num : nums){
sum += num;
}
if(sum % 2 != 0)return false;
int target = sum / 2;
int[] dp = new int[target + 1];
for(int i = 0;i < n;i++){
for(int j = target;j >= nums[i];j--){
dp[j] = Math.max(dp[j],dp[j - nums[i]] + nums[i]);
}
if(dp[target] == target){
return true;
}
}
return dp[target] == target;
}
}