背包问题
0-1 背包问题
给定一个可装载重量为
W
的背包和N
个物品,每个物品有重量和价值两个属性。其中第i
个物品的重量为wt[i]
,价值为val[i]
,现在让你用这个背包装物品,最多能装的价值是多少?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BQFOXKce-1641524249049)(%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98.assets/1.png)]
举个简单的例子,输入如下:
N = 3, W = 4
wt = [2, 1, 3]
val = [4, 2, 3]
算法返回 6,选择前两件物品装进背包,总重量 3 小于 W
,可以获得最大价值 6。
题目就是这么简单,一个典型的动态规划问题。这个题目中的物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。这就是 0-1 背包这个名词的来历。
解决这个问题没有什么排序之类巧妙的方法,只能穷举所有可能,根据我们 动态规划详解 中的套路,直接走流程就行了。
0-1背包问题模板
dp
数组的定义:
dp[i][w]
表示:对于前 i
个物品,当前背包的容量为 w
时,这种情况下可以装下的最大价值是 dp[i][w]
。
如果你没有把这第 i
个物品装入背包,那么很显然,最大价值 dp[i][w]
应该等于 dp[i-1][w]
,继承之前的结果。
如果你把这第 i
个物品装入了背包,那么 dp[i][w]
应该等于 dp[i-1][w - wt[i-1]] + val[i-1]
。
首先,由于 i
是从 1 开始的,所以 val
和 wt
的索引是 i-1
时表示第 i
个物品的价值和重量。
而 dp[i-1][w - wt[i-1]]
也很好理解:你如果装了第 i
个物品,就要寻求剩余重量 w - wt[i-1]
限制下的最大价值,加上第 i
个物品的价值 val[i-1]
。
综上就是两种选择,我们都已经分析完毕,也就是写出来了状态转移方程,可以进一步细化代码:
注意为什么i不从0开始
//01背包
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {//w可以从nums[i]开始
if (w - wt[i-1] < 0) {
// 这种情况下只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
// 装入或者不装入背包,择优
dp[i][w] = max(dp[i -1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]);
}
}
}
return dp[N][W]
完全背包问题
我们可以把这个问题转化为背包问题的描述形式:
有一个背包,最大容量为 amount
,有一系列物品 coins
,每个物品的重量为 coins[i]
,每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?
与0-1的区别在于每个物品数量无限
题解思路
第一步要明确两点,「状态」和「选择」。
状态有两个,就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」嘛,背包问题的套路都是这样。
明白了状态和选择,动态规划问题基本上就解决了,只要往这个框架套就完事儿了:
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 计算(选择1,选择2...)
第二步要明确 dp
数组的定义。
首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维 dp
数组。
dp[i][j]
的定义如下:
若只使用前 i
个物品(可以重复使用),当背包容量为 j
时,有 dp[i][j]
种方法可以装满背包。
换句话说,翻译回我们题目的意思就是:
若只使用 coins
中的前 i
个硬币的面值,若想凑出金额 j
,有 dp[i][j]
种凑法。
经过以上的定义,可以得到:
base case 为 dp[0][..] = 0, dp[..][0] = 1
。因为如果不使用任何硬币面值,就无法凑出任何金额;如果凑出的目标金额为 0,那么“无为而治”就是唯一的一种凑法。
我们最终想得到的答案就是 dp[N][amount]
,其中 N
为 coins
数组的大小。
大致的伪码思路如下:
int dp[N+1][amount+1]
dp[0][..] = 0
dp[..][0] = 1
for i in [1..N]:
for j in [1..amount]:
把物品 i 装进背包,
不把物品 i 装进背包
return dp[N][amount]
第三步,根据「选择」,思考状态转移的逻辑。
注意,我们这个问题的特殊点在于物品的数量是无限的,所以这里和之前写的 0-1 背包问题 文章有所不同。
如果你不把这第 i
个物品装入背包,也就是说你不使用 coins[i]
这个面值的硬币,那么凑出面额 j
的方法数 dp[i][j]
应该等于 dp[i-1][j]
,继承之前的结果。
如果你把这第 i
个物品装入了背包,也就是说你使用 coins[i]
这个面值的硬币,那么 dp[i][j]
应该等于 dp[i][j-coins[i-1]]
。
首先由于 i
是从 1 开始的,所以 coins
的索引是 i-1
时表示第 i
个硬币的面值。
dp[i][j-coins[i-1]]
也不难理解,如果你决定使用这个面值的硬币,那么就应该关注如何凑出金额 j - coins[i-1]
。
比如说,你想用面值为 2 的硬币凑出金额 5,那么如果你知道了凑出金额 3 的方法,再加上一枚面额为 2 的硬币,不就可以凑出 5 了嘛。
综上就是两种选择,而我们想求的 dp[i][j]
是「共有多少种凑法」,所以 dp[i][j]
的值应该是以上两种选择的结果之和:
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++) {
if (j - coins[i-1] >= 0)
dp[i][j] = dp[i - 1][j] + dp[i][j-coins[i-1]];
else
dp[i][w] = dp[i-1][w];
return dp[N][W]
416.分割等和子集
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D72AMYAx-1641524249050)(%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98.assets/image-20211215200302615.png)]
从集合中选出一部分值,达到和的1/2
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
for (int i : nums) {
sum += i;
}
if(sum % 2 != 0) return false;
int W = sum / 2;
int[][] dp = new int[n+1][W+1];
for (int i = 0; i < n; i++){
for (int w = 1; w <= W; w++){
if (w-nums[i]<0){
dp[i+1][w] = dp[i][w];
} else{
dp[i+1][w] = Math.max(dp[i][w-nums[i]] + nums[i], dp[i][w]);
}
}
}
for (int[] row : dp){
if (row[W] == W){
return true;
}
}
return false;
}
优化他的空间复杂度,因为只用到了 i 和 i -1。注意是++还是–
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
for (int i : nums) {
sum += i;
}
if(sum % 2 != 0) return false;
int W = sum / 2;
int[] dp = new int[W+1];
for (int i = 0; i < nums.length; i++){
for (int w = W; w >= nums[i]; w--){
dp[w] = Math.max(dp[w-nums[i]] + nums[i],dp[w]);
}
}
return dp[W] == W;
}
518.零钱兑换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lQNofXeF-1641524249051)(%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98.assets/image-20211215203658477.png)]
完全背包问题
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n+1][amount+1];
for (int[] row : dp)
row[0] = 1;
for (int i = 1; i <= n; i++){
for (int w = 1; w <= amount; w++){
if (w-coins[i-1] >= 0)
dp[i][w] = dp[i-1][w] + dp[i][w-coins[i-1]];
else
dp[i][w] = dp[i-1][w];
}
}
return dp[n][amount];
}
由于只需要i和i-1,所以可以优化空间复杂度。注意是++还是–
public int change(int amount, int[] coins) {
int n = coins.length;
int[] dp = new int[amount+1];
dp[0] = 1;
for (int i = 1; i <= n; i++){
for (int w = coins[i-1]; w <= amount; w++){
dp[w] = dp[w] + dp[w-coins[i-1]];
}
}
return dp[amount];
}