0-1背包问题 – 问题定义
在 0-1 背包问题中,给定一个背包的最大容量 W
,以及 n
个物品,每个物品有两个属性:
- 重量:第
i
个物品的重量为wt[i]
- 价值:第
i
个物品的价值为val[i]
目标是选择若干个物品装入背包,使得在不超过背包最大容量 W
的前提下,装入背包的物品的总价值最大。
注意:0-1 背包中的 “0-1” 指的是每个物品只能被选取一次(即要么选择该物品,要么不选,不存在把物品拆一半放入背包)。
这个0-1背包问题很经典。详情可以买本labuladong的算法笔记这本书看看,或者去网站看。
动态规划解法
背包问题无非就是
状态
+选择
,状态转移方程比较特殊。
第一步:明确状态+选择
状态:只要给几个物品
和一个背包的容量限制
,就形成了一个背包问题呀。所以状态有两个,就是背包的容量和可选择的物品。
选择:装进背包
or 不装进背包
框架:
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
第二步:要明确 dp 数组的定义
-
dp[i][w] 的定义:
dp[i][w]
表示在考虑前i
个物品时,当前背包容量为w
的情况下可以获得的最大价值。- 注意:
i
是从1
开始计数的,意味着i
对应的是第i-1
个物品。
-
两种情况的状态转移:
-
如果不选第
i
个物品:- 那么当前背包的最大价值等于不考虑第
i
个物品时的最大价值,即dp[i][w] = dp[i-1][w]
。这个值继承了前i-1
个物品在背包容量为w
时的最大价值。
- 那么当前背包的最大价值等于不考虑第
-
如果选第
i
个物品:- 你可以将第
i
个物品装入背包,前提是当前背包的容量w
要大于等于该物品的重量wt[i-1]
。 - 在这种情况下,当前物品的总价值应该等于第
i-1
个物品在容量为w - wt[i-1]
时的最大价值,再加上该物品的价值val[i-1]
。 - 公式为:
dp[i][w] = val[i-1] + dp[i-1][w - wt[i-1]]
。
- 你可以将第
-
// ①定义状态
int[][] dp[N+1][W+1]
// ②初始化状态 因为没有物品或者背包没有空间的时候,能装的最大价值就是 0
dp[0][..] = 0
dp[..][0] = 0
// ③状态转移方程
for i in [1..N]:
for w in [1..W]:
dp[i][w] = max(
把物品 i 装进背包,
不把物品 i 装进背包
)
return dp[N][W]
代码
int knapsack(int W, int N, int[] wt, int[] val) {
int N == wt.length;
// 定义状态 dp[i][w] 表示: 对于前 i 个物品(从 1 开始计数),当前背包的容量为 w 时,这种情况下可以装下的最大价值是 dp[i][w]
int[][] dp = new int[N + 1][W + 1];
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {
if (w - wt[i - 1] < 0) {
// 这种情况下只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
// 装入或者不装入背包,择优
dp[i][w] = Math.max(
dp[i - 1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]
);
}
}
}
return dp[N][W];
}
题目:分割等和子集
原题链接: 分割等和子集
题解
方法1:
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]]
:只要有一种情况成立,dp[i][j]
就为true
。即:- 要么不选第
i
个数(dp[i-1][j]
为true
),则前i-1
个数已经能够组成和为j
。 - 要么选择第
i
个数(dp[i-1][j-nums[i-1]]
为true
),则前i-1
个数能组成和为j - nums[i-1]
,加上nums[i-1]
就可以使总和为j
。
- 要么不选第
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) sum += num;
// 和为奇数时,不可能划分成两个和相等的集合
if (sum % 2 != 0) return false;
int target = sum / 2;
// ①定义状态 dp[i][j] 表示前i个数中能否选出若干个 使得和为 j(j为背包容量) 则为true 否则false
boolean[][] dp = new boolean[nums.length + 1][target + 1];
// ②初始化状态
for (int i = 0; i <= nums.length; i++)
dp[i][0] = true; // 背包容量为 0 时 不选任何物品就满足
// ③状态转移
for (int i = 1; i <= nums.length; i++) {
for (int j = 1; j <= target; j++) {
if (j - nums[i - 1] < 0) {
// 背包容量不足,不能装入第 i 个物品
dp[i][j] = dp[i - 1][j];
} else {
// 装入或不装入背包
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
}
}
}
return dp[nums.length][target];
}
这段代码是一个典型的0-1 背包问题的解法,问题是:能否从数组 nums
中找到若干个数,使它们的和等于 target
(即总和的一半)。其中 dp[i][j]
表示前 i
个数能否选出若干个数,使它们的和恰好为 j
。现在解释这一行代码:
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
方法2:
在每次状态转移时,dp[i][j] 只依赖于上一行的状态 dp[i-1][j] 和 dp[i-1][j - nums[i-1]],所以可以将其优化为一维数组 dp[j],从而减少空间复杂度。
在遍历数组时,我们从后向前更新 dp 数组。这样做可以避免同一轮中重复使用同一个元素,即确保每个元素只能使用一次。
public boolean canPartition(int[] nums) {
// 计算数组总和
int sum = 0;
for (int num : nums) {
sum += num;
}
// 如果总和为奇数,无法分成两个子集
if (sum % 2 != 0) return false;
// 目标和是总和的一半
int target = sum / 2;
// ①定义状态 dp[i] 表示是否可以选出若干个元素,使得和为 i
boolean[] dp = new boolean[target + 1];
// ②初始化状态
dp[0] = true;
// ③状态转移
for (int i = 0; i < nums.length; i++) {
for (int j = target; j >= 0; j--) {
if (j - nums[i] >= 0) {
dp[j] = dp[j] | dp[j - nums[i]];
}
}
}
return dp[target];
}
❤觉得有用的可以留个关注~~~❤