【动态规划】详解混合背包问题
目录
- 1. 前置文章
- 2. 题目
- 3. 小结
1. 前置文章
本文前置文章:
- 【动态规划】详解 0-1背包问题
- 【动态规划】详解完全背包问题
- 【动态规划】详解分组背包问题
- 【动态规划】详解多重背包问题
下面是三种背包模式的区别:
0 - 1 背包
是说:有 n 个物品和一个重量为 t 的背包,这 n 个物品中第 i 个物品的重量为 w[i],价值为 v[i],那么这个背包能装下的物品最大价值是多少,注意一个物品只能选一次。完全背包
是说:有 n 个物品和一个重量为 t 的背包,这 n 个物品中第 i 个物品的重量为 w[i],价值为 v[i],那么这个背包能装下的物品最大价值是多少,注意一个物品可以选无数次。分组背包
是说:有 n 组物品和一个重量为 t 的背包,每个物品都有自己的体积,价值和组号,代表这个物品属于哪一组,要求在不超过背包容量的前提下,从每组物品中最多选择一个物品,使得背包中物品的总价值最大。多重背包
是说:有 n 个物品和一个重量为 t 的背包,这 n 个物品中第 i 个物品的重量为 w[i],价值为 v[i],数量为 c[i],那么这个背包能装下的物品最大价值是多少,注意一个物品可以选择的次数限制为 c[i]。
2. 题目
因为混合背包其实就是一道题中包括了 0-1 背包,完全背包,多重背包,这里直接看一道题:
- coins
这道题目意思是:题目输入 n, m,并且给出了这 n 个硬币的价值和个数,问通过这些硬币能够构成 [1,m] 这个价值范围里面的哪些价值。
举个例子:假设 m = 5,现在给了两种硬币,硬币1 价值 1 个数2,硬币2 价值 4 个数 1,问 [1,5] 这个范围里面有多少个价值能够被这些硬币凑出来。
- 首先 1 肯定是可以的,因为价值为1 的硬币有 2 个
- 同理 2 也是可以的
- 3 不可以,因为硬币1 和 硬币2 凑不出来价值 3
- 4 可以,只需要一个硬币2 就行了
- 5 也可以,只需要一个硬币2 和一个硬币 1
所以最终答案就是 5。
这道题就是混合背包的题目了,同样外层遍历物品,内层遍历背包。
- 当
物品个数为 1
,就是 0-1 背包了。 - 当
物品个数 * 价值 > m
,就说明此时这些物品总价值已经超过了上限 m,是完全背包模板。 - 当
物品个数 * 价值 <= m
,就按多重背包来算。
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static int MAXN = 101;
public static int MAXM = 100001;
public static int[] v = new int[MAXN];
public static int[] c = new int[MAXN];
public static boolean[] dp = new boolean[MAXM];
public static int n, m = 0;
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
while (scan.hasNext()) {
n = scan.nextInt();
m = scan.nextInt();
if (n != 0 || m != 0) {
for (int i = 1; i <= n; i++) {
v[i] = scan.nextInt();
}
for (int i = 1; i <= n; i++) {
c[i] = scan.nextInt();
}
}
System.out.println(compute());
}
}
/**
* 有 n 个硬币, 给了出 n 个硬币的价值和个数, 价值相当于重量了
* 再给一个数字 m,求 [1, m] 这个区间内有多少是能够被这些硬币拼凑出来
*
* @return
*/
public static int compute() {
// 每次求之前都初始化一下
Arrays.fill(dp, false);
dp[0] = true;
for (int i = 1; i <= n; i++) {
if (c[i] == 1) {
// 物品只有一个,01 背包
for (int j = m; j >= v[i]; j--) {
if (dp[j - v[i]]) {
dp[j] = true;
}
}
} else if (v[i] * c[i] > m) {
// 完全背包
for (int j = v[i]; j <= m; j++) {
if (dp[j - v[i]]) {
dp[j] = true;
}
}
} else {
// 多重背包: v[i] * c[i] < m
// 因为这里我们需要标记 dp 里面 1~m 哪个是 true,所以单调队列就用一个变量表示 true 的个数就行了
for (int mod = 0; mod <= Math.min(m, v[i] - 1); mod++) {
int trueCount = 0;
int count = 1;
for (int j = m - mod; j >= 0 && count <= c[i]; j -= v[i], count++) {
trueCount += dp[j] ? 1 : 0;
}
for (int j = m - mod, last = j - c[i] * v[i]; j >= 0; j -= v[i], last -= v[i]) {
if (last >= 0) {
trueCount += dp[last] ? 1 : 0;
}
if (dp[j]) {
// 如果已经是 true 了,那么相当于直接删除单调队列里面的过期元素
trueCount--;
} else {
if(trueCount > 0){
dp[j] = true;
}
}
}
}
}
}
int ans = 0;
for (int i = 1; i < dp.length; i++) {
if (dp[i]) {
ans++;
}
}
return ans;
}
}
上面计算多重背包的时候用了 trueCount
代替单调队列,因为我们不需要记录最大值,只需要记录当前 dp[j] 能不能变成 true,只要依赖的几个有一个为 true 那么 dp[j] 就能变成 true。
当然如果 dp[j] 已经是 true 了,那么 j 继续往前遍历,这种情况下 j 下标的值就过期了,所以需要 trueCount--
。
3. 小结
这道题就是混合背包,里面涉及到了 0-1 背包,完全背包,多重背包的流程,还是一样,参考视频:算法讲解075【必备】背包dp-多重背包、混合背包。
如有错误,欢迎指出!!!