题目
4. 多重背包问题 I
5. 多重背包问题 II
6. 多重背包问题 III
有 种物品和一个容量是 的背包。
第 种物品最多有 件,每件体积是 ,价值是 。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,,用空格隔开,分别表示物品种数和背包容积。
接下来有 行,每行三个整数 ,用空格隔开,分别表示第 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
I:
II:
III:
提示:
II:
本题考查多重背包的二进制优化方法。
III:
本题考查多重背包的单调队列优化方法。
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
解题
方法一:动态规划
思路
二维动态规划的思维过程与 完全背包问题 完全相同,只不过多加了个 的条件。
动态规划:
- 状态定义: 表示所有只考虑前 个物品,且总体积不大于 的所有选法中能得到的最大价值。
- 状态转移方程:()。
- 初始状态:只考虑前 个物品的时候没有物品可选,最大价值一定是 。
代码
import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
in.nextToken();
int n = (int) in.nval;
in.nextToken();
int c = (int) in.nval;
int[] v = new int[n + 1], w = new int[n + 1], s = new int[n + 1];
for (int i = 1; i <= n; ++i) {
in.nextToken();
v[i] = (int) in.nval;
in.nextToken();
w[i] = (int) in.nval;
in.nextToken();
s[i] = (int) in.nval;
}
int[][] dp = new int[n + 1][c + 1];
for (int i = 1; i <= n; ++i) {
int vi = v[i], wi = w[i], si = s[i];
for (int j = 0; j <= c; ++j) {
for (int k = 0; k <= si && k * vi <= j; ++k) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k * vi] + k * wi);
}
}
}
System.out.println(dp[n][c]);
}
}
#include <iostream>
using namespace std;
const int N = 110;
int n, c;
int v[N], w[N], s[N], dp[N][N];
int main() {
scanf("%d%d", &n, &c);
for (int i = 1; i <= n; ++i) scanf("%d%d%d", &v[i], &w[i], &s[i]);
for (int i = 1; i <= n; ++i) {
int vi = v[i], wi = w[i], si = s[i];
for (int j = 0; j <= c; ++j) {
for (int k = 0; k <= si && k * vi <= j; ++k) {
dp[i][j] = max(dp[i][j], dp[i - 1][j - k * vi] + k * wi);
}
}
}
printf("%d\n", dp[n][c]);
return 0;
}
时间复杂度: ( 含义如题, 指 的最大值)
优化一:二进制优化 + 01背包一维优化
尝试与 完全背包问题 - 优化 一样代换状态转移方程进行优化:
原式:f[i][j] = max(f[i-1][j-k*vi] + k*wi)
展开:f[i][j] = max(f[i-1][j], f[i-1][j-vi]+wi, f[i-1, j-2*vi]+2*wi, ..., f[i-1][j-s*vi]+s*wi)
f[i][j-vi] = max( f[i-1][j-vi], f[i-1, j-2*vi]+wi, ..., f[i-1][j-s*vi]+(s-1)*wi, f[i-1][j-(s+1)*vi]+s*wi)
发现无法进行代换,这种优化方式行不通。
此时有另外一种优化方式:
例如目前 物品的体积是 ,价值是 ,数量是 。
那么我们可以把 物品分解为:
- 物品 ,体积: ,价值:,数量:。
- 物品 ,体积: ,价值:,数量:。
- 物品 ,体积: ,价值:,数量:。
- 物品 ,体积: ,价值:,数量:。
- 物品 ,体积: ,价值:,数量:。
- 物品 ,体积: ,价值:,数量:。
根据二进制的性质,有了以上这些物品我们可以构造出 个 物品中的任意一个数量,比如说:
,,, ……
如果物品的数量不是 怎么办?
——比如 物品的体积是 ,价值是 ,数量是 。
那么我们可以把 物品分解为:
- 物品 ,体积: ,价值:,数量:。
- 物品 ,体积: ,价值:,数量:。
- 物品 ,体积: ,价值:,数量:。
- 物品 ,体积: ,价值:,数量:。
- 物品 ,体积: ,价值:,数量:。
- 物品 ,体积: ,价值:,数量:。
- 物品 ,体积: ,价值:,数量:,其中:。
这样下来就可以构造出 个 物品中的任意一个数量。
我们发现这样就可以把任意一种数量为 的物品分解成 种数量为 的物品。
这样以来我们就把条件从「给定 种物品,第 种物品最多有 件,体积是 ,价值是 」变成了「给定 种物品,每件物品只能使用一次,第 种物品的体积是 ,价值是 」。把问题从 多重背包 转换成了 0-1背包,代码直接套用 0-1背包的代码,时间复杂度降到了 。
import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
in.nextToken();
int n = (int) in.nval;
in.nextToken();
int c = (int) in.nval;
int m = n * 12;
int[] v = new int[m + 1], w = new int[m + 1];
int idx = 0;
for (int i = 1; i <= n; ++i) {
in.nextToken();
int vi = (int) in.nval;
in.nextToken();
int wi = (int) in.nval;
in.nextToken();
int si = (int) in.nval;
int x = 1;
while (x <= si) {
++idx;
v[idx] = vi * x;
w[idx] = wi * x;
si -= x;
x <<= 1;
}
if (si > 0) {
++idx;
v[idx] = vi * si;
w[idx] = wi * si;
}
}
n = idx;
int[] dp = new int[c + 1];
for (int i = 1; i <= n; ++i) {
int vi = v[i], wi = w[i];
for (int j = c; j >= vi; --j) {
dp[j] = Math.max(dp[j], dp[j - vi] + wi);
}
}
System.out.println(dp[c]);
}
}
#include <iostream>
using namespace std;
const int N = 1010, M = 2010, L = N * 11;
int n, c;
int v[L], w[L], dp[M];
int main() {
scanf("%d%d", &n, &c);
int idx = 0;
for (int i = 1; i <= n; ++i) {
int vi, wi, si;
scanf("%d%d%d", &vi, &wi, &si);
int x = 1;
while (x <= si) {
++idx;
v[idx] = vi * x;
w[idx] = wi * x;
si -= x;
x <<= 1;
}
if (si > 0) {
++idx;
v[idx] = vi * si;
w[idx] = wi * si;
}
}
n = idx;
for (int i = 1; i <= n; ++i) {
int vi = v[i], wi = w[i];
for (int j = c; j >= vi; --j) {
dp[j] = max(dp[j], dp[j - vi] + wi);
}
}
printf("%d\n", dp[c]);
return 0;
}
优化二:单调队列优化+拷贝数组优化
import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
in.nextToken(); int n = (int) in.nval;
in.nextToken(); int c = (int) in.nval;
int[] f = new int[c + 1], g;
int[] que = new int[c + 1];
int hh, tt;
for (int i = 0; i < n; ++i) {
in.nextToken(); int v = (int) in.nval;
in.nextToken(); int w = (int) in.nval;
in.nextToken(); int s = (int) in.nval;
g = f.clone(); // 拷贝上一层状态
// 枚举余数
for (int r = 0; r < v; ++r) {
hh = 0; tt = -1;// 清空优先队列
// 从余数开始枚举空间
for (int j = r; j <= c; j += v) {
// 将超出窗口范围的元素出队(`j - que[hh] / v`表示滑动窗口中的元素数量)
while (hh <= tt && (j - que[hh]) / v > s) ++hh;
// 当前状态比队尾元素表示的状态更优 队尾元素没有存在必要 队尾出队
// 注意: 队尾元素需要加上价值偏移量: `(j - que[tt]) / v * w`
while (hh <= tt && g[j] >= g[que[tt]] + (j - que[tt]) / v * w) --tt;
// 当前下标入队
que[++tt] = j;
// 更新当前这一层的状态(注意依旧要加上价值偏移量)
f[j] = g[que[hh]] + (j - que[hh]) / v * w;
}
}
}
System.out.println(f[c]);
}
}
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010, V = 20010;
int n, c;
int f[2][V];
int que[V];
int hh, tt = -1;
int main() {
scanf("%d%d", &n, &c);
for (int i = 1; i <= n; ++i) {
int v, w, s; scanf("%d%d%d", &v, &w, &s);
int curr = i & 1, prev = curr ^ 1;
for (int r = 0; r < v; ++r) {
hh = 0, tt = -1;
for (int j = r; j <= c; j += v) {
while (hh <= tt && (j - que[hh]) / v > s) ++hh;
while (hh <= tt && f[prev][j] >= f[prev][que[tt]] + (j - que[tt]) / v * w) --tt;
que[++tt] = j;
f[curr][j] = f[prev][que[hh]] + (j - que[hh]) / v * w;
}
}
}
printf("%d\n", f[n & 1][c]);
return 0;
}
评论区