46. 携带研究材料(第六期模拟笔试)
题目描述
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。
小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。
输入描述
第一行包含两个正整数,第一个整数 M 代表研究材料的种类,第二个正整数 N,代表小明的行李空间。
第二行包含 M 个正整数,代表每种研究材料的所占空间。
第三行包含 M 个正整数,代表每种研究材料的价值。
输出描述
输出一个整数,代表小明能够携带的研究材料的最大价值。
输入示例
6 1
2 2 3 1 5 2
2 3 1 5 4 3
输出示例
5
提示信息
小明能够携带 6 种研究材料,但是行李空间只有 1,而占用空间为 1 的研究材料价值为 5,所以最终答案输出 5。
数据范围:
1 <= N <= 5000
1 <= M <= 5000
研究材料占用空间和价值都小于等于 1000
方法一:
public class BagProblem {
public static void main(String[] args) {
int[] weight = {1,3,4};
int[] value = {15,20,30};
int bagSize = 4;
testWeightBagProblem(weight,value,bagSize);
}
/**
* 动态规划获得结果
* @param weight 物品的重量
* @param value 物品的价值
* @param bagSize 背包的容量
*/
public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
// 创建dp数组
int goods = weight.length; // 获取物品的数量
int[][] dp = new int[goods][bagSize + 1];
// 初始化dp数组
// 创建数组后,其中默认的值就是0
for (int j = weight[0]; j <= bagSize; j++) {
dp[0][j] = value[0];
}
// 填充dp数组
for (int i = 1; i < weight.length; i++) {
for (int j = 1; j <= bagSize; j++) {
if (j < weight[i]) {
/**
* 当前背包的容量都没有当前物品i大的时候,是不放物品i的
* 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
*/
dp[i][j] = dp[i-1][j];
} else {
/**
* 当前背包的容量可以放下物品i
* 那么此时分两种情况:
* 1、不放物品i
* 2、放物品i
* 比较这两种情况下,哪种背包中物品的最大价值最大
*/
dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
}
}
}
// 打印dp数组
for (int i = 0; i < goods; i++) {
for (int j = 0; j <= bagSize; j++) {
System.out.print(dp[i][j] + "\t");
}
System.out.println("\n");
}
}
}
这段Java代码实现了一个基于动态规划(Dynamic Programming, DP)的方法来解决经典的“0-1背包问题”。0-1背包问题是指有一个背包,最大承重为bagSize
,同时有goods
件物品,每件物品都有自己的重量weight[i]
和价值value[i]
。目标是确定每件物品放或不放的选择方案,使得放入背包的物品总价值最大,同时不超过背包的承重限制。
解析
-
初始化DP数组:
dp[i][j]
表示在只考虑前i
件物品的情况下,当背包容量为j
时能装入物品的最大总价值。- 初始化第一行时,考虑的是只有第一件物品时的情况,因此当背包容量大于等于第一件物品的重量时,可以选择放入该物品,
dp[0][j] = value[0]
。
-
填充DP数组:
- 外层循环遍历所有物品。
- 内层循环遍历从0到背包最大容量
bagSize
的所有可能容量。 - 对于每个
dp[i][j]
,有两种选择:- 不放入第
i
件物品,此时的最大价值等于前i-1
件物品在容量为j
时的最大价值,即dp[i-1][j]
。 - 如果放入第
i
件物品(前提是当前背包容量j
大于等于第i
件物品的重量weight[i]
),则需要从背包剩余容量中减去当前物品的重量,查看剩余容量下的最大价值,即dp[i-1][j-weight[i]] + value[i]
,然后与不放该物品的情况比较取最大值。
- 不放入第
-
打印DP数组:
- 最后,通过双层循环遍历并打印出整个
dp
数组,帮助我们直观地理解每一步决策过程及最终结果。
- 最后,通过双层循环遍历并打印出整个
结果
虽然代码中包含了打印dp
数组的过程以供观察,但实际应用中,我们通常只关心最终结果,即dp[goods-1][bagSize]
,它表示在考虑所有物品和背包容量限制下能够获取的最大价值。不过,这段代码未直接输出这个结果,如果你需要看到具体的最优解,可以在打印数组之后添加一行代码输出dp[goods-1][bagSize]
。
方法二:
import java.util.Arrays;
public class BagProblem {
public static void main(String[] args) {
int[] weight = {1,3,4};
int[] value = {15,20,30};
int bagSize = 4;
testWeightBagProblem(weight,value,bagSize);
}
/**
* 初始化 dp 数组做了简化(给物品增加冗余维)。这样初始化dp数组,默认全为0即可。
* dp[i][j] 表示从下标为[0 - i-1]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
* 其实是模仿背包重量从 0 开始,背包容量 j 为 0 的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为 0。
* 可选物品也可以从无开始,也就是没有物品可选,即dp[0][j],这样无论背包容量为多少,背包价值总和一定为 0。
* @param weight 物品的重量
* @param value 物品的价值
* @param bagSize 背包的容量
*/
public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
// 创建dp数组
int goods = weight.length; // 获取物品的数量
int[][] dp = new int[goods + 1][bagSize + 1]; // 给物品增加冗余维,i = 0 表示没有物品可选
// 初始化dp数组,默认全为0即可
// 填充dp数组
for (int i = 1; i <= goods; i++) {
for (int j = 1; j <= bagSize; j++) {
if (j < weight[i - 1]) { // i - 1 对应物品 i
/**
* 当前背包的容量都没有当前物品i大的时候,是不放物品i的
* 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
*/
dp[i][j] = dp[i - 1][j];
} else {
/**
* 当前背包的容量可以放下物品i
* 那么此时分两种情况:
* 1、不放物品i
* 2、放物品i
* 比较这两种情况下,哪种背包中物品的最大价值最大
*/
dp[i][j] = Math.max(dp[i - 1][j] , dp[i - 1][j - weight[i - 1]] + value[i - 1]); // i - 1 对应物品 i
}
}
}
// 打印dp数组
for(int[] arr : dp){
System.out.println(Arrays.toString(arr));
}
}
}
这段代码是使用Java实现的解决0-1背包问题的改进版本,通过动态规划(Dynamic Programming, DP)方法计算在给定背包容量和每件物品的重量、价值的情况下,能够获取的最大价值。相比之前的版本,这里的实现做了些微调和优化,特别是对动态规划数组的初始化进行了简化,并在代码中增加了详细的注释来解释每一步的操作。下面是代码的详细解析:
代码解析
-
初始化改进:在定义
dp
数组时,增加了一个冗余维度,使得物品编号从1到goods
对应于数组下标0到goods-1
,这样的好处是在计算时不需要特别处理边界情况,直接遍历即可,简化了逻辑。dp[i][j]
的含义是考虑前i
件物品,背包容量为j
时的最大价值。 -
简化初始化:由于
dp
数组被初始化为全0,这已经符合了动态规划的初始条件——即没有物品可选或背包容量为0时,价值总和为0。因此,去除了显式的初始化步骤,直接进入填充阶段。 -
填充DP数组:
- 双重循环遍历每一件物品和每一个可能的背包容量。对于每个状态
dp[i][j]
:- 若当前背包容量
j
不足以容纳第i
件物品(即j < weight[i-1]
),则不选第i
件物品,价值继承自不选此物品的最大价值,即dp[i][j] = dp[i-1][j]
。 - 若背包容量足够,需要在不选第
i
件物品(价值dp[i-1][j]
)和选择第i
件物品(价值dp[i-1][j-weight[i-1]] + value[i-1]
)中取最大值,以最大化背包总价值。
- 若当前背包容量
- 双重循环遍历每一件物品和每一个可能的背包容量。对于每个状态
-
打印结果:最后,通过遍历并打印
dp
数组,可以直观地查看每一步动态规划的决策过程,虽然在实际应用中可能并不需要打印,但有助于理解和调试。
结果分析
代码执行后,dp[goods][bagSize]
即为所能获取的最大价值。不过,需要注意的是,代码最后打印的是整个dp
数组的状态,而不是直接输出最大价值。如果需要直接获取并打印最大价值,可以在循环结束后添加一行代码,如System.out.println(dp[goods][bagSize]);
。
方法三:
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWight = 4;
testWeightBagProblem(weight, value, bagWight);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
int wLen = weight.length;
//定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
int[] dp = new int[bagWeight + 1];
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 0; i < wLen; i++){
for (int j = bagWeight; j >= weight[i]; j--){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//打印dp数组
for (int j = 0; j <= bagWeight; j++){
System.out.print(dp[j] + " ");
}
}
这段Java代码实现了一个简化版的0-1背包问题的动态规划解法。0-1背包问题的目标是:给定一组物品,每种物品都有自己的重量和价值,在限定的总重量内,选取哪些物品可以使得总价值最大。下面是对这段代码的详细解析:
主函数
- 定义了物品的重量数组
weight
、价值数组value
以及背包的最大承重bagWeight
。 - 调用
testWeightBagProblem
方法求解最大价值。
testWeightBagProblem 方法
-
初始化:首先获取物品的数量
wLen
,并定义一个长度为bagWeight + 1
的数组dp
。dp[j]
表示当背包容量为j
时,能够装入物品的最大总价值。 -
双重循环:
- 外层循环遍历每个物品(从0到
wLen-1
)。 - 内层循环从背包的最大容量
bagWeight
反向遍历到当前物品的重量。这样的遍历顺序保证了在计算dp[j]
时,已经处理了所有比当前物品轻的物品,利用了之前计算的结果,体现了动态规划的“状态转移”。
- 外层循环遍历每个物品(从0到
-
状态转移方程:在内层循环中,对于每个背包容量
j
,有两种选择:- 不放入第
i
件物品,此时背包的最大价值保持不变,即dp[j] = dp[j]
。 - 放入第
i
件物品,背包的当前容量减去该物品的重量j - weight[i]
,背包的最大价值变为之前的最大价值加上当前物品的价值,即dp[j - weight[i]] + value[i]
。 - 使用
Math.max()
函数选择这两种情况中的较大值作为新的dp[j]
,表示在考虑放入第i
个物品后,容量为j
的背包能够达到的最大价值。
- 不放入第
-
打印结果:最后,代码遍历并打印
dp
数组,展示在不同背包容量下能够达到的最大价值。
注意点
- 这个版本的代码通过逆序遍历背包容量,避免了需要初始化第一排和第一列的麻烦,直接从空背包(容量为0)开始计算,逐渐增加背包容量。
- 由于是从大到小遍历背包容量,所以在考虑放入当前物品时,之前容量较小的情况已经计算完成,可以直接利用,保证了动态规划的“从已知求未知”的原则。
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
方法一:
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--) {
//物品 i 的重量是 nums[i],其价值也是 nums[i]
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
//剪枝一下,每一次完成內層的for-loop,立即檢查是否dp[target] == target,優化時間複雜度(26ms -> 20ms)
if(dp[target] == target)
return true;
}
return dp[target] == target;
}
}
这段Java代码是用于解决“零钱兑换II”或类似问题的一个变种,准确地说,是判断一个整数数组nums
中的元素是否可以分成两个子集,使得这两个子集的和相等。这是一种完全背包问题的变体,用于判断是否存在子集和为总和一半的情况。以下是代码的详细解析:
方法概览
- 输入:一个整数数组
nums
。 - 输出:如果可以将数组分割成两个总和相等的子集,则返回
true
,否则返回false
。
核心逻辑
- 检查特殊情况:首先,检查数组是否为空或长度为0,若是,则直接返回
false
,因为不可能分割。 - 计算总和:计算数组元素的总和
sum
,如果sum
是奇数,则直接返回false
,因为不能平均分配。 - 目标值:设置目标值
target
为总和的一半。 - 初始化动态规划数组:创建一个长度为
target + 1
的数组dp
,用于存储达到每个目标和的可能性。初始化所有值为0,因为一开始没有任何物品被选中。 - 填充动态规划数组:双层循环遍历每个物品和每个可能的目标和值。从数组中的每个元素开始,反向遍历到目标值,更新
dp[j]
为max(dp[j], dp[j - nums[i]] + nums[i])
。这一步是在考虑是否选择当前物品nums[i]
以达到或更接近目标和j
。 - 剪枝优化:在内层循环结束后,检查是否已经找到了和为目标值的子集(
dp[target] == target
),如果是,则提前返回true
,避免不必要的循环。 - 返回结果:最后,检查
dp[target]
是否等于target
,如果等于则说明找到了满足条件的子集,返回true
;否则返回false
。
性能优化
- 剪枝:通过在内层循环结束后进行检查,一旦发现已达到目标和,就提前终止循环,这是一种有效的剪枝策略,可以减少不必要的计算,提高程序运行效率。
总的来说,这个方法通过动态规划有效地解决了能否将数组分割成两个和相等的子集的问题,同时通过剪枝策略进行了优化。
方法二 :二维数组版本(易于理解)
public class Solution {
public static void main(String[] args) {
int num[] = {1,5,11,5};
canPartition(num);
}
public static boolean canPartition(int[] nums) {
int len = nums.length;
// 题目已经说非空数组,可以不做非空判断
int sum = 0;
for (int num : nums) {
sum += num;
}
// 特判:如果是奇数,就不符合要求
if ((sum %2 ) != 0) {
return false;
}
int target = sum / 2; //目标背包容量
// 创建二维状态数组,行:物品索引,列:容量(包括 0)
/*
dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数
每个数只能用一次,使得这些数的和恰好等于 j。
*/
boolean[][] dp = new boolean[len][target + 1];
// 先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满 (这里的dp[][]数组的含义就是“恰好”,所以就算容积比它大的也不要)
if (nums[0] <= target) {
dp[0][nums[0]] = true;
}
// 再填表格后面几行
//外层遍历物品
for (int i = 1; i < len; i++) {
//内层遍历背包
for (int j = 0; j <= target; j++) {
// 直接从上一行先把结果抄下来,然后再修正
dp[i][j] = dp[i - 1][j];
//如果某个物品单独的重量恰好就等于背包的重量,那么也是满足dp数组的定义的
if (nums[i] == j) {
dp[i][j] = true;
continue;
}
//如果某个物品的重量小于j,那就可以看该物品是否放入背包
//dp[i - 1][j]表示该物品不放入背包,如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true;
//dp[i - 1][j - nums[i]]表示该物品放入背包。如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。
if (nums[i] < j) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
for (int i = 0; i < len; i++) {
for (int j = 0; j <= target; j++) {
System.out.print(dp[i][j]+" ");
}
System.out.println();
}
return dp[len - 1][target];
}
}
//dp数组的打印结果
false true false false false false false false false false false false
false true false false false true true false false false false false
false true false false false true true false false false false true
false true false false false true true false false false true true
这段Java代码实现了求解“分割等和子集”问题的动态规划算法。给定一个非空数组nums
,判断是否可以将其分割成两个子集,使得两个子集的和相等。以下是代码的详细解析:
主要逻辑
-
初始化:首先,计算数组的总和
sum
,并检查总和是否为偶数。如果是奇数,则直接返回false
,因为无法分割成两个和相等的子集。接着,设定目标和target
为总和的一半。 -
创建DP数组:定义一个布尔类型的二维数组
dp
,其中dp[i][j]
表示在前i
个元素中是否存在一些元素的和等于j
。数组的大小是len
(数组长度)乘以target + 1
,初始化为false
。 -
初始化DP数组的第一行:如果数组的第一个元素不大于目标和
target
,则dp[0][nums[0]]
置为true
,表示可以恰好装满容量为nums[0]
的背包。 -
填充DP数组:通过两层循环遍历每个物品和每个可能的目标和。对于每个元素,有两种情况考虑:
- 如果当前物品的值等于当前的容量
j
,那么可以单独装入背包,dp[i][j] = true
。 - 如果当前物品的值小于当前的容量
j
,那么有两种选择:不选当前物品(继承上一行相同容量的状态dp[i-1][j]
)或者选当前物品(查看剩余容量j-nums[i]
在上一行是否可以被满足,即dp[i-1][j-nums[i]]
)。如果这两种情况有任何一种为真,则dp[i][j] = true
。
- 如果当前物品的值等于当前的容量
-
输出和返回结果:最后,打印整个
dp
数组(用于调试查看状态转移过程),并返回dp[len - 1][target]
,即判断数组最后一个元素对应的背包容量target
是否可以被满足。
输出解释
打印的dp
数组展示了动态规划过程中每个状态的真假值,其中true
表示存在一个子集的和等于当前列索引所表示的值。最后一行的最后一个元素(即dp[len - 1][target]
)为true
,表明原数组可以分割成两个和为target
的子集。
综上所述,这段代码有效地利用动态规划求解了“分割等和子集”问题。
方法三:二维数组整数版本
class Solution {
public boolean canPartition(int[] nums) {
//using 2-D DP array.
int len = nums.length;
//check edge cases;
if(len == 0)
return false;
int sum = 0;
for (int num : nums)
sum += num;
//we only deal with even numbers. If sum is odd, return false;
if(sum % 2 == 1)
return false;
int target = sum / 2;
int[][] dp = new int[nums.length][target + 1];
// for(int j = 0; j <= target; j++){
// if(j < nums[0])
// dp[0][j] = 0;
// else
// dp[0][j] = nums[0];
// }
//initialize dp array
for(int j = nums[0]; j <= target; j++){
dp[0][j] = nums[0];
}
for(int i = 1; i < len; i++){
for(int j = 0; j <= target; j++){
if (j < nums[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]);
}
}
//print out DP array
// for(int x : dp){
// System.out.print(x + ",");
// }
// System.out.print(" "+i+" row"+"\n");
return dp[len - 1][target] == target;
}
}
//dp数组的打印结果 for test case 1.
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 6, 6,
0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 6, 11,
0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 10, 11,
这段Java代码实现了解决“分割等和子集”问题的动态规划算法。给定一个非空整数数组nums
,判断是否可以将其分割成两个子集,使得两个子集的和相等。以下是代码的详细解释:
算法思路
-
预处理:首先计算数组
nums
的总和sum
,并检查sum
是否为偶数。如果是奇数,则直接返回false
,因为无法均分为两个和相等的子集。如果为偶数,将目标和target
设置为sum
的一半。 -
初始化动态规划表:创建一个二维数组
dp
,其中dp[i][j]
表示在前i
个元素中是否存在子集的和等于j
。数组的大小为nums.length
乘以target + 1
。注意,对于第一行的初始化有误,正确的初始化应该考虑nums[0]
是否小于等于当前列索引j
。 -
填充动态规划表:遍历数组
nums
的每个元素,对于每个元素,从target
开始反向遍历到nums[i]
(包括),更新dp[i][j]
的值。有两种情况:- 如果当前背包容量
j
小于当前物品nums[i]
的值,那么不选当前物品,状态继承自上一行,即dp[i][j] = dp[i - 1][j]
。 - 否则,可以选择当前物品或者不选,取两者中能使得子集和等于
j
的最大可能性,即dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i])
。
- 如果当前背包容量
-
返回结果:最后,检查
dp[len - 1][target]
是否等于target
,如果等于则说明找到了一个子集和等于target
,返回true
;否则返回false
。
注意点
- 代码中注释掉的部分是原始的错误初始化示例,正确的初始化逻辑已经在循环中通过条件判断实现了。
- 另外,代码中还注释掉了打印
dp
数组的调试语句,这在开发过程中可用于观察动态规划表的构建过程,帮助理解算法的工作原理。
通过以上步骤,该算法有效地解决了给定数组是否可以分割成两个和相等的子集的问题。