目录
题目1: 01 背包理论基础(二维)
- 题目链接:01-背包
1- 01 背包
- 有 n 件物品和一个最多能背重量为
w
的背包。第i
件物品的重量是weight[i]
,得到的价值是value[i]
。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。 - 这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解了。
- 这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢?
- 每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),这里的 n 表示物品数量。
- 所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!
在下面的讲解中,我举一个例子:
- 背包最大重量为4。
- 物品为:
- 问背包能背的物品最大价值是多少?
- 以下讲解和图示中出现的数字都是以这个例子为例。
2- 动规:二维 dp 数组解决 01 背包
依然动规五部曲分析一波。
- 1. 确定dp数组以及下标的含义
- 对于背包问题,有一种写法, 是使用二维数组,即 dp[i][j] 表示从下标为[0-i] 的物品里任意取,放进容量为 j 的背包,价值总和最大是多少。
- 只看这个二维数组的定义,大家一定会有点懵,看下面这个图:
- 要时刻记着这个dp数组的含义,下面的一些步骤都围绕这 dp 数组的含义进行的,如果哪里看懵了,就来回顾一下i代表什么, j 又代表什么。
- 2. 确定递推公式
- 再回顾一下
dp[i][j]
的含义:从下标为[0-i]
的物品里任意取,放进容量为j
的背包,价值总和最大是多少。 - 那么可以有两个方向推出来
dp[i][j]
, - 不放物品i:由
dp[i - 1][j]
推出,即背包容量为j
,里面不放物品i
的最大价值,此时dp[i][j]
就是dp[i - 1][j]
。(其实就是当物品i
的重量大于背包j
的重量时,物品i
无法放进背包中,所以背包内的价值依然和前面相同。) - 放物品i:由
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] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
- 3. dp数组如何初始化
- 关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
- 首先从dp[i][j]的定义出发,如果背包容量 j 为 0 的话,即 dp[i][0],无论是选取哪些物品,背包价值总和一定为 0 。如图:
在看其他情况。
状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
可以看出 i
是由 i-1
推导出来,那么i
为0
的时候就一定要初始化。
dp[0][j]
,即:i
为0
,存放编号0
的物品的时候,各个容量的背包所能存放的最大价值。
那么很明显当 j < weight[0]
的时候,dp[0][j]
应该是 0
,因为背包容量比编号0
的物品重量还小。
当 j >= weight[0]
时,dp[0][j]
应该是value[0]
,因为背包容量放足够放编号0物品。
代码初始化如下:
for (int j = 0 ; j < weight[0]; j++) { // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。
dp[0][j] = 0;
}
// 正序遍历
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
此时dp数组初始化情况如图所示:
其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。
初始-1,初始-2,初始100,都可以!
但只不过一开始就统一把dp数组统一初始为0,更方便一些。
如图:
- 4. 确定遍历顺序
- 由于 dp 数组横向是 背包容量 纵向是 物品
- 这里采用 **_先遍历物品重量,再遍历背包容量 _**的方式进行遍历
- 遍历物品:由于此时下标为 0 的物品已经初始化过,所以 i 从 1 开始遍历 物品
- 遍历背包:此时遍历背包的大小,则 j 从 0 开始遍历,遍历到背包的容量即 j <= bagweight
- ① 如果 当前背包没有物品重,此时 dp[i][j] 应该为
dp[i][j] = dp[i - 1][j]
- ② 否则 将 i 放入背包,此时
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
- 5. 举例推导 dp 数组
- 最终结果就是dp[2][4]。
3- 题解
⭐ 01 背包理论基础——题解思路
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int m = sc.nextInt();
int n = sc.nextInt();
// 1. 定义 dp 数组
int[][] dp = new int[m][n+1];
int[] weight = new int[m];
for(int i = 0 ; i < m;i++){
weight[i] = sc.nextInt();
}
int[] value = new int[m];
for(int i = 0 ; i < m;i++){
value[i] = sc.nextInt();
}
// 2. 递推公式 dp[i][j]
// 2.1 如果当前 (背包容量) < (物品重量) 即 j < weight[i] ——> dp[i][j] = dp[i-1][j]
// 2.2 否则 dp[i][j] 为 Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
// 3. 初始化
// 3.1 初始化第一列
for(int i = 0 ; i < m;i++){
dp[i][0] = 0;
}
// 3.2 初始化第一行
for(int i = 0 ; i <= n;i++){
if(i>=weight[0]){
dp[0][i] = value[0];
}
}
// 4. 遍历顺序
// 先遍历 物品 再遍历 背包
for(int i = 1 ; i < m;i++){
for(int j = 1 ; j<=n;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[m-1][n]);
// return dp[m-1][n];
}
}
题目2: 01 背包理论基础(一维)
- 题目链接:01-背包
1- 思路
动规五部曲
- 1. 定义 dp 数组以及其含义
dp[j]
代表 容量为 j 的背包的最大价值
- 2. 递推公式
dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i])
- 3. dp 数组初始化
- dp 数组初始化为非 0 中的最小值
- 4.确定遍历顺序
- 先遍历背物品: 商品从
i = 0
开始 遍历到 m - 再遍历背包:背包从
j = 背包大小
开始 遍历到weight[i]
- 先遍历背物品: 商品从
2- 题解
⭐ 01 背包理论基础(一维)——题解思路
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int m = sc.nextInt();
int n = sc.nextInt();
// 1. 定义 dp 数组
int[] dp = new int[n+1];
int[] weight = new int[m];
for(int i = 0 ; i < m;i++){
weight[i] = sc.nextInt();
}
int[] value = new int[m];
for(int i = 0 ; i < m;i++){
value[i] = sc.nextInt();
}
// 2. 递推公式 dp[i][j]
// 3. 初始化
dp[0] = 0;
// 4. 遍历顺序
// 先遍历 物品 再遍历 背包
for(int i = 0 ; i < m;i++){
for(int j = n ; j>=weight[i];j--){
dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i]);
}
}
System.out.println(dp[n]);
// return dp[m-1][n];
}
}
题目3: 分割等和子集
- 题目链接:416. 分割等和子集
1- 思路
数组分割为背包问题,比如求 [1,5,11,5] 数组中是否能分为两个和为 11 的数组。
- 可以将该问题抽象为 : 容量为 11 的背包能不能装满
动规五部曲
- 1. 定义 dp 数组确定 dp 数组含义
int[] dp
:代表容量为 j 的数组包含的最大价值- 题目提供的数组 即代表容量 也代表最大价值
- 本题的返回条件:
dp[target] == target
此时返回true
- target的求法:先对数组求和,对求和结果 / 2 得到的就是 target
- 2. 确定递推公式,即状态转移方程
dp[j] = Math.max(dp[j],dp[j-weight[i]] + value[j])
- 此时状态转移方程类似于 0 1 背包问题中的压缩方式
- 3. 初始化 dp 数组
- dp[0] =0 其他是非负数的最小值 也就是 0
- 4. 确定遍历顺序
- 4.1 先遍历商品:
- i 从 0 到 n
- 4.2 再遍历背包:
- j 从 target 到 weight[i] ,此时商品的重量和价格看作是一样的
- 背包需要反向遍历,因为每个元素我仅使用一次
- 4.1 先遍历商品:
2- 题解
⭐ 分割等和子集——题解思路
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
for(int a:nums){
sum+=a;
}
//总和为奇数,不能平分
if(sum % 2 != 0) return false;
int target = sum / 2;
// 1. 定义 dp数组,含义:容量为 j 的背包最大的价值
int[] dp = new int[target+1];
// 2. 递推公式 dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i])
// 3. 初始化dp 数组
dp[0] = 0;
// 4. 遍历顺序
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[j] == target){
return true;
}
}
}
return false;
}
}