比赛迫在眉睫,而我依旧这么菜。。
蓝桥杯对dp情有独钟,临阵磨枪不快也光,所以今天就来补一下dp,首先就是经典的背包问题。
(我还是太弱了,这状态转移方程想了很久才做出来......在excel上一个一个推着去理解的....)
讲的可能会比较啰嗦,主要是为了照顾跟我一样的新手小伙伴,当然也是自己推导解题方法的过程
上题目!
资源限制
内存限制:256.0MB C/C++时间限制:1.0s Java时间限制:3.0s Python时间限制:5.0s
问题描述
给定N个物品,每个物品有一个重量W和一个价值V.你有一个能装M重量的背包.问怎么装使得所装价值最大.每个物品只有一个.
输入格式
输入的第一行包含两个整数n, m,分别表示物品的个数和背包能装重量。
以后N行每行两个数Wi和Vi,表示物品的重量和价值
输出格式
输出1行,包含一个整数,表示最大价值。
样例输入
3 5
2 3
3 5
4 7
样例输出
8
数据规模和约定
1<=N<=200,M<=5000.
老规矩,先整理一下思路。
我们用 数组W 存物品的重量,用 数组V 存物品的价值,一个M(int)代表背包容量
首先说明一下,用dp就是为了优化时间,在数据量大的情况下爆搜容易超时,那如果用枚举呢,这里想说的是,所有枚举都是错误的(看了一个b站博主的解释),为什么呢,接下来举三个例子
1.按价值大的优先放入背包
M = 3
W[] = {1,2,3}
V[] = {2,2,3}
枚举结果:2 实际结果:3
解读:优先选择价值高的,会选择3号物品,此时背包已经满了不能继续装了,实际是要选择1号与2号为最优解(下面内容同理,就不需要解读了)
2.优先放入重量小的物品
M = 3
W[] = {1,2,3}
V[] = {1,2,4}
枚举结果:3实际结果:4
3.优先选择性价比最高的(即单位容量的价值)
M = 4
W[] = {2,2,3}
V[] = {2,2,5}
性价比:1 1 0.6
枚举结果:4实际结果:5
看出为什么枚举行不通了吧,因此就可以知道dp的重要性
这里开始思路讲解
我们设一个数组dp,设 i 为行,j 为列,我们用 i 表示挑选前 i 个物品,j 表示背包大小,而数组元素 dp[i][j] 则代表在只挑选前 i 件物品的条件下重量为 j 的最大价值。
那接下来就是每个物品选与不选的问题了,选第 i 件物品就是dp[i][j]=dp[i-1][j-W[i]]+V[i] (当背包容量为j的情况下)
即选的情况下,第 i 件物品的价值等于第 i 件物品的价值 加上 j 减去 第 i 件物品的重量 得到的剩余可以装的重量在选i-1件物品中可以得到的最大价值。
不选即 dp[i][j] = dp[i-1][j] ,即直接继承上一个状态,就是选前 i-1 件物品的最大价值
关于选的问题可能会有新人看着比较绕,这里再仔细说一下
比如当前选择的是 i 件物品,背包容量为 j ,此物品重量为 x ,当装下 i 件物品时背包容量剩 j - x ,那这个j - x 容量能装下的物品价值加上 第 i 件物品的价值不就是 选择 i 件物品的价值了吗。
附上excel表格推测图 与 测试运行结果
当i = 0 , j = 0 时初始化全部为0,第一方便状态转移,第二当选0件物品跟背包容量0当然价值都是0了。
接下来上代码
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();//n件物品
int m = scan.nextInt();//背包最大容量
int[] W = new int[n+1];//储存物品所占空间
int[] V = new int[n+1];//物品价值
/*
用i表示装前i件物品,j表示背包容量
*/
int dp[][] = new int[n+1][m+1];//状态转移
for(int i = 1 ; i<=n ; i++){
W[i] = scan.nextInt();
V[i] = scan.nextInt();
}
for(int i = 1 ; i<=n ; i++){//从选择前1件物品遍历
for(int j = 1 ; j<=m ; j++){//从容量为1时遍历
/*
因为可能出现第i件物品容量要大于背包容量的问题,此时背包装不下第i件物品
装不下就是不选,直接继承上一个状态
*/
if(j-W[i]>=0){
dp[i][j] = Math.max(dp[i-1][j-W[i]]+V[i],dp[i-1][j]);
}else {
dp[i][j] = dp[i-1][j];
}
}
}
// 调试结果是否与推测相同
// for(int i = 0 ; i<=n ; i++){
// System.out.println(Arrays.toString(dp[i]));
// }
System.out.println(dp[n][m]);
}
}
其实通过以上内容我们可以看出来,当前状态 i 只与上一个状态 i - 1有关,我们完全可以用一维写法,用循环不断遍历更新最优解即可,此时 i 只有一层了,j 不变。
一维即优化内存,状态转移方程:选,dp[j] = dp[j-W[i]] + V[i] 不选 ,dp[j] 不变
上代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int M = scan.nextInt();
int W[] = new int[n];
int V[] = new int[n];
int dp[] = new int[M+1];
for(int i = 0 ; i<n ; i++){
W[i] = scan.nextInt();
V[i] = scan.nextInt();
}
for(int i = 0 ; i<n ; i++){
for(int j = M ; j>=W[i] ; j--){//装不下就直接退出循环
//选与不选的问题
dp[j] = Math.max(dp[j-W[i]]+V[i],dp[j]);//不断更新最优解
}
}
System.out.println(dp[M]);
}
}
提交一下,都过了
其实状态转移就这么短,可做起来怎么这么绕呢。。。