DP问题简单是真简单,难是真的难,刚学算法被dp折磨的一天做一道题都费劲,主要还是要学会分析题意,找出状态转移公式,不过y总的分析法一直也没怎么看懂,大部分时候还是看灵性
背包问题属于经典DP问题了,01背包算式背包中最简单的一种,一般会和其他知识点结合出现,贴道模板题
AcWing 2. 01背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
题意:也就是大多数01背包问题的统一题意,给出n个物品,每个物品带有一个重量v和价值w,再给出一个最大容量为m的背包,求在各种组合中能使背包中所放物品价值最大的值
dp问题的核心思想就是从前向后推出最终结果并记录,类似递推的思想,当随机选取一个状态–i个物品、j为最大容量时,可以判断当前大体上具有两种情况,一种是最大容量j不足以存放第i个物品,即j < v [ i ] ,当发生这种情况时,无论如何都不可能放进第i个物品,可以将其舍去,令(i,j)时的值等于(i-1,j)时的值。
另一种情况,当容量j足以存放第i件物品时,此时又分两种情况,选择或不选择:若不选择第i件物品,则与上述情况相同,(i,j)的值即是(i-1,j)的值。当选择时,就对容量和价值进行处理,由于存放了第i件物品,容量变为 j - v[ i ],此时情况变为,在前(i-1)个物品中,以j - v[ i ]的容量,再次获取最大值,因此此时(i,j)的值,变为(i-1,j- v[ i ])+w[ i ]。
在容量足够的两种情况中,不能保证放进 i 件物品就一定是最大值,因此对选与不选两种情况进行取max操作
最终状态方程即为
dp[ i ][ j ]=max(dp[ i-1 ][ j ], dp[ i-1 ][ j-v[ i ] ]+w[ i ]);
代码如下
import java.io.*;
import java.util.*;
public class Main {
static Scanner tab = new Scanner(System.in);
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
static int N = 1010;
public static void main(String[] args) throws IOException {
int dp[][]=new int [N][N];//(物品数量,最大容量)
int v[]=new int [N];
int w[]=new int [N];
int n=tab.nextInt();//数量
int m=tab.nextInt();//容量
for(int i=1;i<=n;i++) {
v[i]=tab.nextInt();
w[i]=tab.nextInt();
}
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
if(j>=v[i])//最大容量能够装下第i件物品
dp[i][j]=Math.max(dp[i-1][j], dp[i-1][j-v[i]]+w[i]);
else
dp[i][j]=dp[i-1][j];
}
}
System.out.println(dp[n][m]);
}
}
常规01背包代码就是这些,但当01背包与其他算法结合出现时,一个较大的二维dp数组可能会卡空间,需要简化空间复杂度。
另一方面观察状态方程,可以看到dp[i][j]的只与它的上一层dp[i][~]有关,因此可以将其简化成一维,dp[j]代表容量为 j 时的最大价值。
这里的简化思想可以参考前缀和的简化,一般来说,前缀和数组公式为s[i]=a[i]+s[i-1],但这里a[0]~a[i-1]的值完全是不使用的,可以直接将前缀和存储在a[i]中,用前缀和将原数组覆盖过去:a[i]=a[i-1]+a[i]
有点牵强,大体思想即是当考虑完第i-1个物品的各种情况后,后续的计算是无需使用0~(i-2)值的,因此直接将i-1的各值存在数组中,覆盖前有的数值,用于后续第i个物品的计算即可。而当第i个物品计算后,同理将i-1的值覆盖,继续第i+1物品的计算
但此题需要注意的一个点是,由于原代码是从小到大遍历,将dp数组简化一维后将其抽象为一个横向数组,在对第 i 个物品计算时,每次对dp[ j ]的计算都需要使用的是其左侧的值,如果从左至右遍历的话,当计算一个dp[ j ]的值时,需要使用的左侧值已经更新为第 i 层,而计算所需要的是第i-1层的数据,它已经被覆盖了,继续计算的话会使结果偏大,因此在简化代码中需从右向左遍历计算,即是从大容量从小容量遍历,来保证数据的准确
非常想贴个图上来,可惜没什么好画图的软件,建议在纸上模拟一遍简化代码的实现过程,再模拟一遍从左向右遍历的错误过程,比较容易理解
代码如下
for(int i=1;i<=n;i++) {
for(int j=m;j>=v[i];j--) {//直接减至v[i]可以少个判断语句
dp[j] = Math.max(dp[j], dp[j-v[i]] + w[i]);
}
}
虽说是道入门题,可以说是入的相当费劲了