摘要
本文主要介绍了01背包理论基础(一)、01背包理论基础(二)(滚动数组)和LeetCode416题分割等和子集。
1、01背包理论基础(一)
1.1 题目描述
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
1.2 思路
-
dp数组以及下标的含义:dp[i][j]表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少 -
递推公式:
-
第一种情况:放不下物品,0-i的物品, j容量可以放下的最大价值等于0-(i-1)的物品,j-1容量的价值
dp[i][j] = dp[i-1][j]
-
第二种情况:可以放下物品,由
dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]]为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i](物品i的价值),就是背包放物品i得到的最大价值dp[i][j] = Math.max(dp[i-1][j], value[i] + dp[i-1][j-weight[i]])
-
如果
j < items[i].weight则放不下物品,反之可以放下物品
-
-
dp数组如何初始化:-
首先从
dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。 -
dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值 。- 当 j < weight[0]的时候,
dp[0][j]应该是 0,因为背包容量比编号0的物品重量还小 - j >= weight[0]时,
dp[0][j]应该是items[0].value,因为背包容量放足够放编号0物品
- 当 j < weight[0]的时候,
-
-
dp数组遍历顺序: 先遍历物品,然后遍历背包重量 -
打印
dp数组
1、为什么 创建 dp 数组是 int[][] dp = new int[n][m + 1] ?
dp[i][j]代表从 [0,i] 的物品任取,放入重量为 j 的背包的最大价值为dp[i][j],因为n=items.length所以 i=0 代表第1个物品,所以 i 的取值范围是 [0, n-1],j = 0 代表背包重量为0,j = m代表背包的最大容量,所以j 的取值范围是[0, m],综上所述int[][] dp = new int[n][m + 1]
2、为什么 i = 0 时,当 j >= items[0].weight,dp[0][j] == items[0].value?
因为 i 等于0 代表从一个物品中取,放入背包容量为 j 的背包,只有当前物品可取,所以容量 j 大于当前物品的重量时,最大价值为
items[0].value
3、为什么动态规划中需要判断 j < items[i].weight ?
首先如果
j < items[i].weight表示放不下当前物品,所以dp[i][j] = dp[i-1][j],只有当j >= items[i].weight时,才可以放下当前物品,所以可以选择放下当前物品或不放下当前物品dp[i][j] = Math.max(dp[i-1][j], value[i] + dp[i-1][j--weight[i]])
1.3 代码
public class Rucksack {
public static void main(String[] args) {
Item item1 = new Item(1, 15);
Item item2 = new Item(3, 20);
Item item3 = new Item(4, 30);
Item[] items = {item1, item2, item3};
int m = 4;
rucksack(items, m);
}
// dp[i][j] 代表从 [0,i] 的物品任取,放入重量为 j 的背包的最大价值为 dp[i][j]
// 不取物品 dp[i][j] = dp[i-1][j]
// 取物品 dp[i][j] = Math.max(dp[i-1][j], items[i].value + dp[i-1][j-items[i].weight])
// 如果j < j-items[i].weight 则不取物品,反之可以取物品
// 初始化 dp[i][0] = 0; 如果 j >= items[0].weight; dp[0][j] = items[0].value
public static void rucksack(Item[] items, int m) {
int n = items.length;
int[][] dp = new int[n][m + 1];
for (int j = m; j >= 0; j--) {
if (j < items[0].weight) {
break;
}
dp[0][j] = items[0].value;
}
// 先遍历物品,然后遍历背包
for (int i = 1; i < n; i++) {
for (int j = 1; j <= m; j++) {
if (j < items[i].weight) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], items[i].value + dp[i - 1][j - items[i].weight]);
}
}
}
// 打印
print(dp, n, m);
}
public static void print(int[][] dp, int n, int m) {
for (int i = 0; i < n; i++) {
for (int j = 0; j <= m; j++) {
System.out.print(dp[i][j] + "\t");
}
System.out.println("\n");
}
}
public static class Item {
public int weight;
public int value;
public Item() {
}
public Item(int weight, int value) {
this.weight = weight;
this.value = value;
}
}
}
2、01背包理论基础(二)(滚动数组)
2.1 思路
-
dp数组以及下标的含义: 在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。 -
递推公式:
-
第一种情况:放不下物品,0-i的物品, j容量可以放下的最大价值等于0-(i-1)的物品,j-1容量的价值
dp[j] = dp[j]
-
第二种情况:可以放下物品,由
dp[j - weight[i]]推出,dp[j - weight[i]]为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[j - weight[i]] + value[i](物品i的价值),就是背包放物品i得到的最大价值dp[j] = Math.max(dp[j], value[i] + dp[j-weight[i]])
-
如果
j < items[i].weight则放不下物品,反之可以放下物品
-
-
dp数组如何初始化:-
首先从
[j]的定义出发,如果背包容量j为0的话,即dp[0],无论是选取哪些物品,背包价值总和一定为0。 -
dp[j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值 。- 当 j < weight[0]的时候,
dp[j]应该是 0,因为背包容量比编号0的物品重量还小 - j >= weight[0]时,
dp[j]应该是items[0].value,因为背包容量放足够放编号0物品
- 当 j < weight[0]的时候,
-
dp数组遍历顺序:先遍历物品,然后遍历背包,但背包是倒序遍历- 打印
dp数组
1、为什么会出现一维dp数组?
使用二维数组的时候,递推公式:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]),使用一维数组时,递推公式:dp[j] = Math.max(dp[j], value[i] + dp[j-weight[i]])。只用dp[j](一维数组,也可以理解是一个滚动数组),这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
2、为什么一维dp数据,遍历背包是倒序遍历?
倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。
2.2 代码
public class Rucksack2 {
public static void main(String[] args) {
Item item1 = new Item(1, 15);
Item item2 = new Item(3, 20);
Item item3 = new Item(4, 30);
Item[] items = {item1, item2, item3};
int m = 4;
rucksack(items, m);
}
// 滚动数组就是把上一层数组拷贝下来了
// dp[j] 放入重量为 j 的背包的最大价值为 dp[j]
// 不放物品 dp[j] = dp[j]
// 放物品 dp[j] = Math.max(dp[j], items[i].value + dp[j-items[i].weight])
// 如果j < j-items[i].weight] 则不取物品,反之可以取物品
// 初始化,如果 j >= items[0].weight; dp[j] = items[0].value
public static void rucksack(Item[] items, int m) {
int n = items.length;
int[] dp = new int[m + 1];
for (int j = m; j >= 0; j--) {
if (j < items[0].weight) {
break;
}
dp[j] = items[0].value;
}
// 先遍历物品,然后遍历背包,但背包是倒序遍历
for (int i = 1; i < n; i++) {
for (int j = m; j >= 1; j--) {
if (j < items[i].weight) {
dp[j] = dp[j];
} else {
dp[j] = Math.max(dp[j], items[i].value + dp[j - items[i].weight]);
}
}
}
// 打印
print(dp, n, m);
}
public static void print(int[] dp, int n, int m) {
for (int j = 0; j <= m; j++) {
System.out.print(dp[j] + "\t");
}
}
public static class Item {
public int weight;
public int value;
public Item() {
}
public Item(int weight, int value) {
this.weight = weight;
this.value = value;
}
}
}
3、416.分割等和子集
3.1 思路
1、套用01背包需要确定哪些步骤?
- 确定背包的体积
- 确定物品的重量和价值
- 是否不可重入放入
2、如果套用01背包问题到本题中?
dp[j] 表示装满容量为j的背包的最大价值为dp[j],背包的容量是target = sum / 2,物品的重量和价值都为 num[i],因为放入物品到背包中是不会超过背包的体积的,所以如果装满背包,即dp[target] = target,则满足条件,可以分割等和子集
3.2 代码
public boolean canPartition(int[] nums) {
int sum = getSum(nums);
int target = sum / 2;
if(sum % 2 != 0) {
return false;
}
int[] dp = new int[target+1];
for(int j=target; j>=1; j--) {
if(j < nums[0]) {
break;
}
dp[j] = nums[0];
}
for(int i=1; i<nums.length; i++) {
for(int j=target; j>=1; j--) {
if(j < nums[i]) {
dp[j] = dp[j];
} else {
dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
}
}
}
return dp[target] == target;
}
public int getSum(int[] nums) {
int sum = 0;
for(int num : nums) {
sum += num;
}
return sum;
}