426. 开心的金明
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。
更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 N N N元钱就行”。
今天一早金明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的 N N N元。
于是,他把每件物品规定了一个重要度,分为 5 5 5等:用整数 1 1 1~ 5 5 5表示,第 5 5 5等最重要。
他还从因特网上查到了每件物品的价格(都是整数元)。
他希望在不超过 N N N元(可以等于 N N N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第 j j j件物品的价格为 v [ j ] v[j] v[j],重要度为 w [ j ] w[j] w[j],共选中了 k k k件物品,编号依次为 j 1 , j 2 , … , j k j_1,j_2,…,j_k j1,j2,…,jk,则所求的总和为:
v
[
j
1
]
∗
w
[
j
1
]
+
v
[
j
2
]
∗
w
[
j
2
]
+
…
+
v
[
j
k
]
∗
w
[
j
k
]
v[j_1]∗w[j_1]+v[j_2]∗w[j_2]+…+v[j_k]∗w[j_k]
v[j1]∗w[j1]+v[j2]∗w[j2]+…+v[jk]∗w[jk]
请你帮助金明设计一个满足要求的购物单。
输入格式
输入文件的第
1
1
1行,为两个正整数
N
N
N和
m
m
m,用一个空格隔开。(其中
N
N
N表示总钱数,
m
m
m为希望购买物品的个数)
从第 2 2 2行到第 m + 1 m+1 m+1行,第 j j j行给出了编号为 j − 1 j-1 j−1的物品的基本数据,每行有 2 2 2个非负整数 v v v和 p p p。(其中 v v v表示该物品的价格, p p p表示该物品的重要度)
输出格式
输出文件只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(数据保证结果不超过
100000000
100000000
100000000)。
数据范围
1
≤
N
<
30000
,
1≤N<30000,
1≤N<30000,
1
≤
m
<
25
,
1≤m<25,
1≤m<25,
0
≤
v
≤
10000
,
0≤v≤10000,
0≤v≤10000,
1
≤
p
≤
5
1≤p≤5
1≤p≤5
输入样例:
1000 5
800 2
400 5
300 5
400 3
200 2
输出样例:
3900
思路:
将原问题做如下转化
- 总钱数相当于背包总容量;
- 每件物品的价格相当于体积;
- 每件物品的价格乘以重要度相当于价值;
那么就变成了经典的01背包问题。
状态计算: f[i][j]
表示所有从前i
个物品选,且总体积不超过j
的选法集合中的价值最大值
那么f[m][n]
就表示从前m
种物品中选,且总体积不超过n
的所有选法集合的价值最大值,即为答案。
集合划分:
按照第i
种物品选或者不选划分 f[i][j]
集合。
-
不选第
i
种物品,f[i][j] = f[i - 1][j]
;
问题转化为从前i - 1
个物品选,且总体积不超过j
的选法集合中的最大值。 -
选第
i
种物品,f[i][j] = f[i - 1][j - v] + v * w
;
已经确定选第i
种物品,那么问题转化为从前i - 1
个物品选,且总体积不超过j - v
的选法集合中的最大值再加上 当前物品的价值v * w
。
状态计算方程:
f[i][j] = max(f[i - 1][j],f[i - 1][j - v] + v * w)
Java代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();//总共n元钱
int m = scanner.nextInt();//m件物品
int[] v = new int[m + 1];//每件物品的价格
int[] w = new int[m + 1];//每件物品的重要度
int[][] f = new int[m + 1][n + 1];
for(int i = 1;i <= m;i++){
v[i] = scanner.nextInt();
w[i] = scanner.nextInt();
}
//初始化f数组,因为后面会出现i-1,故i需要大于等于1,故初始化i为0的情况,然后让i从1开始
//事实上他们默认就是0,不过写上引起注意,记住有这个细节
for(int j = 0;j < n;j++){
f[0][j] = 0;
}
//现在i可以从1开始了
for(int i = 1;i <= m;i++){
for(int j = 0;j <= n;j++){
f[i][j] = f[i-1][j];
if(j >= v[i]){
f[i][j] = Math.max(f[i - 1][j],f[i-1][j - v[i]] + v[i] * w[i]);
}
}
}
System.out.println(f[m][n]);
}
}
考虑优化为一维DP
可先看完day15 1371 货币系统(完全背包问题)的优化为一维DP部分,然后再看这里的优化部分,相互补充,理解更为深刻。
假设你已经看完了day15 1371 货币系统(完全背包问题),好的,现在开始优化本题。
我们不难发现状态计算时f[i]
层的更新只用到了f[i-1]
层,因此我们可以去掉前一维
状态计算方程变为 :f[j] = max(f[j], f[j-v]+v*w);
代码表示:
for(int j = v[i]; j <= n; j++)
{
f[j] = Math.max(f[j], f[j-v[i]]+v[i]*w[i]);
}
但是此时不和之前的代码f[i][j] = Math.max(f[i - 1][j],f[i-1][j - v[i]] + v[i] * w[i])
等价了,因为在计算第i
层的状态时,我们从小到大枚举体积,体积j-v[i]
严格小于j
,那么f[j-v[i]]
会先于f[j]
被计算出来,此时我们这里的f[j-v[i]]
已经为第i
层状态,这样f[j-v[i]]
等价于f[i][j-v[i]]
,实际上我们需要的是f[j-v[i]]
得等价于f[i-1][j-v[i]]
才行。
为了解决这个问题只需要体积从大到小枚举,
for(int j = n ; j >= v[i]; j--)
{
f[j] = Math.max(f[j], f[j-v[i]]+v[i]*w[i]);
}
因为我们从大到小枚举体积,而j - v[j]
严格小于j
我们在计算f[j]
的时候f[j-v[i]]
还未被第i
层状态更新过,那么它存的就是上一层(i-1层)
的状态,即f[i-1][j-v[i]]
。
Java代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();//总共n元钱
int m = scanner.nextInt();//m件物品
int[] v = new int[m + 1];//每件物品的价格
int[] w = new int[m + 1];//每件物品的重要度
int[] f = new int[n + 1];
for(int i = 1;i <= m;i++){
v[i] = scanner.nextInt();
w[i] = scanner.nextInt();
}
for(int i = 1;i <= m;i++){
for(int j = n;j >=v[i]; j--){
f[j] = Math.max(f[j],f[j - v[i]] + v[i] * w[i]);
}
}
System.out.println(f[n]);
}
}