(题目来自AcWing)
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是wi,求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0
0
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例
10
完全背包问题的特点就是不限每个物品的个数
正如之前所说,动态规划分为两步:状态表示与状态计算
不再多说,定义dp[i][j] 这个集合表示的就是所有选法。
表示前i个物品中选,总体积为j的所有选法的最大值
这一点和01背包一致,这个问题的特点在于,我们的第 i 个物品 是无限的,这将会影响到我们的状态计算。见下:
正如上一个博客所讲,我们的状态计算就是 在进行集合划分。
01背包问题我们可以分为 0 和 1,也就是要第 i 个物品,或者不要
那么这个问题中无限个数的 第 i 个 物品 ,我们该如何划分呢?
我们可以把这个划分为 k 个集合,集合(t >= 0 && t <= k) 分别代表选 t 个第 i 个物品的选法
这个 k 就是全选物品 i 的不溢出数字, 否则 k 将失去意义。
即 k 要满足:当全部选择物品 i 时,不能让这个背包溢出:
也就是 k * volume[i] <= j
那我们思路就很清晰了,有了 01背包的经历,我们很容易想到:
选择 i 个物品 , 背包容量为 j 时,
集合 ki(k >= 0 && k * volume[i] <= j)的值,此时选了 t 个物品 i ,用和 01背包问题选择 物品i 的集合情况 一样的思考方式,就可以转化为 dp[i - 1][j - k * volume[i]] + k * weigh[i]
,
然后在 fork 循环遍历中,找到这些集合的最大值,就是dp[i][j] 的所要的最大值。
可写出朴素做法:
for (int i = 1; i <= n; ++i) {
for (int j = 0; j < m; ++j) {
for (int k = 0; k * volume[i] <= j; ++k) {
//最多k个,也就是全装上第 i 个
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k * volume[i]] + k * weight[i]);
}
}
}
过了!
但是耗时太长(这个提交我甚至还用了快速读入)
让我们想想如何优化
上边的为什么慢呢?因为有三重循环
老样子,让我们观察状态转移方程
(以下用 v 表示 volumn(i), w 表示 weigh[i] )
dp[i][j] 的值是 k 个集合(k为物品 i 的个数)的最大值
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v] + w, dp[i - 1][j - 2v] + 2w, dp[i - 1][j - 3v] + 3w,…)
再让我们看看dp[i ][j - v] 和它做对比
dp[i][j - v] = max(
dp[i - 1][j - v],dp[i - 1][j - v] , dp[i - 1][j - 2v] + w, dp[i - 1][j - 3v] + 2w,…)
我们可以发现,dp[i][j] 和 dp[i][j - v] 只是相差了w。这很好想
有集合A,B
若A中数字均大于B中数字w,那么集合A,B的最大值也一定相差w。
其实从他们两个各自代表的实际意义也能知道,只相差一个物品 i 的价值。
那么我们就能推断出新的状态转移方程:
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - v] + w);
让我们试一试,就能写出:
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
dp[i][j] = dp[i - 1][j];
if (j >= volume[i]) {
dp[i][j] = Math.max(dp[i][j], dp[i][j - volume[i]] + weight[i]);
}
}
}
过了!
快了很多很多!
但是我们还可以再快一点
直接做等价变形,删去一维的 i 就可以,
dp[i][j] = dp[i - 1][j];
变成dp[j] = dp[j];
毫无意义,直接删去即可
又因为下边的if条件,直接让 j 从 volumn[i] 开始遍历
状态转移方程删去一维部分就变成
dp[j] = Math.max(dp[j], dp[j - volume[i]] + weight[i]);
就变成
for (int i = 1; i <= n; ++i) {
for (int j = volume[i]; j <= m; ++j) {
dp[j] = Math.max(dp[j], dp[j - volume[i]] + weight[i]);
}
}
过啦!
更快!
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
Input in = new Input();
int n = in.nextInt();
int m = in.nextInt();
int[] volume = new int[n + 1]; //体积(重量)
int[] weight = new int[n + 1]; //价值(权重)
for (int i = 1; i <= n; ++i) {
volume[i] = in.nextInt();
weight[i] = in.nextInt();
}
//分的集合是选择第i个物品选择第k个来划分,且 k * volume[i] <= 容量j
/** 朴素做法*/
// int[][] dp = new int[n + 1][m + 1];
/* for (int i = 1; i <= n; ++i) {
for (int j = 0; j < m; ++j) {
for (int k = 0; k * volume[i] <= j; ++k) { //最多k个,也就是全部装第 i 个
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k * volume[i]] + k * weight[i]);
}
}
}
*/
/** 二维优化做法*/
/* for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
dp[i][j] = dp[i - 1][j];
if (j >= volume[i]) {
dp[i][j] = Math.max(dp[i][j], dp[i][j - volume[i]] + weight[i]);
}
}
}
System.out.println(dp[n][m]);*/
/**一维优化做法*/
int[] dp = new int[m + 1];
for (int i = 1; i <= n; ++i) {
for (int j = volume[i]; j <= m; ++j) {
dp[j] = Math.max(dp[j], dp[j - volume[i]] + weight[i]);
}
}
System.out.println(dp[m]);
}
}
class Input {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
public int nextInt() throws IOException {
in.nextToken();
return (int) in.nval;
}
}