接下来会开始不同的背包问题, 而01背包问题指的是, 有多个物品, 但是每个物品只能有一个, 这里的暴力解法就是回溯, 因为每种物品只有取与不取两种状态, 但是接下来会用dp降低时间复杂度
二维数组解法
- dp[i][j]的含义, [0,i]物品任取放入容量为[j]的背包
- 递推数组:
- 不放物品i, dp[i - 1][j], 意思就是背包此时已满, 到达最大价值, 但是i没有放进去
- 放物品i, dp[i - 1][j - weight[i]]+value[i], 背包容量没放i时的最大价值+i的价值
- 初始化: 当j为weight[0]时, dp[0][j]应该是value[0], 也就是当背包里只有第一个物品时, 重量也就只等于第一个物品
- 遍历顺序: 第一次for循环遍历
public class Main {
public static void main(String[] args) {
//先初始化一下题目
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagSize = 4;
solveProblem(weight, value, bagSize);
}
public static void solveProblem(int[] weight, int[] value, int bagSize){
int wlen = weight.length;
//创建dp数组
int[][] dp = new int[wlen + 1][bagSize+1];
//初始化dp
//就记住当只放0号物品时, 背包重量就是weight[0]
//这里是小于bagsize, 因为j就是定义的bag
for (int j = weight[0]; j <= bagSize; j++){
dp[0][j] = value[0];
}
//开始递推, 格外注意这里的小于等于号
for(int i = 1; i < wlen; i++){
for(int j = 1; j <= bagSize; j++) {
//先来个判断, 如果这个背包的size比物品size还小了, 说明放不下
//那么此时最优值就是没放这个物品的情况
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]);
}
}
}
//完成动态之后, 就把最后结果打印出来
for(int i = 0; i < wlen; i++){
for(int j = 0; j <= bagSize; j++){
System.out.print(dp[i][j] + " ");
}
System.out.println("");
}
}
}
滚动一维数组(优先使用)
- dp[j]数组含义: 容量为j的背包最大价值为dp[j]
- 递推公式: dp[j] = Math.max(dp[j]), dp[j-weight[i]+value[i]) 不放物品i, 就是dp[j], 把上一层数组拷贝过来
- 初始化dp[0]=0, 就是背包容量为0时就是0
- 遍历顺序: 二维的时候背包容量是从小到大, 一维的时候遍历dp是从大到小倒序遍历, 保证每个数组只被遍历过一次
public class Main {
public static void main(String[] args) {
//先初始化一下题目
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagSize = 4;
solveProblem(weight, value, bagSize);
}
public static void solveProblem(int[] weight, int[] value, int bagSize){
int wlen = weight.length;
//创建一维dp数组
int[] dp = new int[bagSize+1];
//开始递推, 这里利用滚动数组
for(int i = 0; i < wlen; i++){
//这里的倒序尤其需要注意, 而且是大于等于weight
for(int j = bagSize; j >= weight[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//完成动态之后, 就把最后结果打印出来
for(int j = 0; j <= bagSize; j++){
System.out.println(dp[j] +" ");
}
}
}
分割等和子集
其实根据题目可以看出来可以用回溯, 但是会超时, 所以重点就是把这道题和背包联系起来:
- 背包的体积为sum
- 背包要放入的商品重量为元素的数值, 价值也为元素的数值
- 背包如果正好装满, 说明找到了总和为sum/2的子集
- 背包中的每一个元素是不可重复放入
(相当于一个容量为sum/2的背包, 尝试数字是否能全部放进这个背包里, 物品的重量和价值都是nums[i])
动规五部曲:
- dp[j], 容量为j的背包
- 递推公式, 和之前一样
- 初始化: 非0下标的元素初始化为0就可以了
- 遍历顺序: 由于这里还是滚动数组, 所以注意下j的遍历
class Solution {
public boolean canPartition(int[] nums) {
//首先可以剪个枝
if(nums == null || nums.length == 0){
return false;
}
int sum = 0;
int nlen = nums.length;
//先求出sum
for(int num : nums){
sum += num;
}
//特判: 如果是奇数的话就不能平分
if(sum % 2 != 0) {
return false;
}
//求出target, 也就是sum/2
int target = sum/2;
int[] dp = new int[target + 1];
//开始递推
for(int i = 0; i < nlen; i++){
for(int j = target; j >= nums[i]; j--){
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[target] == target;
}
}