背包问题
01背包
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
暴力解法
每个物品只有两种状态,用or不用。所以暴力解法时间复杂度为 暴力解法是指数级别的复杂度,很显然需要优化
动态规划
二维dp数组
用如下数据举例: 背包容量为4,共有3个物品
| 名称 | 重量 | 价值 |
|---|---|---|
| 物品0 | 1 | 10 |
| 物品1 | 3 | 25 |
| 物品2 | 4 | 30 |
既然是动态规划,我们先考虑动态规划的五部曲:
- 确定dp数组(dp table)以及下标的含义:
dp[i][j]指任取0~i号物品放入容量为j的背包里,可达到的最大价值。 - 确定递推公式:对于任取0~i号物品放入容量为j的背包,为了有最大价值。如果第i件不放入背包,
dp[i][j]就为dp[i-1][j]。如果第i件放入(且可以放入),dp[i][j]等于第i件的价值➕dp[i-1][j-w[i]]。只有这两种情况,所以dp[i][j]是这两个值中间的最大值。 - dp数组如何初始化:考虑对
dp[i][j]的定义和递推公式,需要初始化的是dp[0][j]。考虑dp数组的定义,如果j>=w[0],dp[0][j]初始化为v[0],否则为0 - 确定遍历顺序:从上到下,从左到右遍历
- 举例推导dp数组:对上面的例子,推导dp数组如下
| 0 | 10 | 10 | 10 | 10 |
|---|---|---|---|---|
| 0 | 10 | 10 | 25 | 35 |
| 0 | 10 | 10 | 25 | 35 |
一维滚动数组
- dp数组和下标的含义:dp[j]就是容量为j的背包里能到的最大价值
- 确定递推公式
在二维递推公式的基础上,去掉维度i,就可以得到一个一维递推公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);对其含义进行解读:dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值。 - dp数组如何初始化:根据数组的含义dp[0]一定为0,其他的为了不影响递推,也初始化0.可以理解为不放入任何物品的行。
- 确定遍历顺序:由于我们把第i-1行复制到了第i行,如果从左到右遍历,会覆盖前一行的左侧数据。所以从右到左遍历,这样dp[j]需要的数据都在左侧,而左侧还是上一行的数据
- 举例推导dp数组:同上
kama 46 携带研究材料
题目链接:kamacoder.com/problempage…
思路
和上述01背包相同
解法
二维数组版
import java.util.Scanner;
public class Main{
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int m = scanner.nextInt(); // 物品个数
int n = scanner.nextInt(); // 背包容量
int[] weight = new int[m];
int[] value = new int[m];
for (int i = 0; i < m; i++) {
weight[i] = scanner.nextInt();
}
for (int i = 0; i < m; i++) {
value[i] = scanner.nextInt();
}
int[][] dp = new int[m][n+1];
// 初始化dp数组
for (int i = 0; i <= n; i++) {
if (i >= weight[0]) {
dp[0][i] = value[0];
}
}
for (int i = 1; i < m; i++) {
for (int j = 0; j <= n; j++) {
if (weight[i] <= j) {
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
}
else {
dp[i][j] = dp[i-1][j];
}
}
}
System.out.println(dp[m-1][n]);
}
}
一维滚动数组版
import java.util.Scanner;
public class Main{
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int m = scanner.nextInt(); // 物品个数
int n = scanner.nextInt(); // 背包容量
int[] weight = new int[m];
int[] value = new int[m];
for (int i = 0; i < m; i++) {
weight[i] = scanner.nextInt();
}
for (int i = 0; i < m; i++) {
value[i] = scanner.nextInt();
}
int[] dp = new int[n+1];
// 初始化dp数组
for (int i = 0; i <= n; i++) {
dp[i] = 0;
}
for (int i = 0; i < m; i++) {
for (int j = n; j >= 0; j--) {
if (weight[i] <= j) {
dp[j] = Math.max(dp[j], dp[j-weight[i]] + value[i]);
}
else {
dp[j] = dp[j];
}
}
}
System.out.println(dp[n]);
}
}
LeetCode 416 分割等和子集
思路
本题的本质是:求能否装满容量为sum/2的背包,sum是整个数组的和
考虑一个数字的重量和价值,重量对应容量,所以重量是数字本身;同时需要让背包在sum/2的维度里最大,所以价值也对应数字本身。
所以这个背包问题是,给一个容量为sum/2的背包,装到价值最大,价值是否为sum/2
于是我们把本题转化为了背包问题,考虑动态规划五部曲:
- dp数组和下标含义:dp[j]为容量为j的背包能装下的数字的最大和
- 确定递推公式:
dp[j] = max(dp[j], dp[j-nums[i]]+nums[i]) - dp数组如何初始化:一个数字都不放,都初始化为0
- 确定遍历顺序:从右向左
- 举例推导dp数组:假设数组为
[1,5,11,5],每一行代表一次迭代.sum为22,sum/2为11
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 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 | 6 | 11 |
解法
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
if (sum % 2 == 1) {
return false;
}
int[] dp = new int[sum/2 + 1];
for (int i = 0; i < nums.length; i++) {
for (int j = sum/2; j >= 0; j--) {
if (nums[i] <= j) {
dp[j] = Math.max(dp[j], dp[j-nums[i]]+nums[i]);
}
}
}
if (dp[sum/2] == sum/2) {
return true;
}
return false;
}
}
今日收获总结
解决背包问题,需要搞清楚价值和容量在题中的含义,然后用动态规划五部曲解决