Acwing算法基础课动态规划第一节——背包问题。个人认为还算记得比较细,有需要的小伙伴可以看看我的一些思路~自己先在Typora做的笔记导进来的,图片大小没有调整,主要是备份笔记用的。
01背包
问题描述
有n个物品,每个物品体积为v[i],价值为w[i];背包体积容量m。每个物品只能装入背包一次,问满足不超过背包体积的情况下的最大价值。
思路分析
用 f[i] [j] 表示:前 i 个物品中,总体积不超过 j 的最大价值,则f [n] [m]就是我们要求的答案。
求 f[i] [j] 可以分成两个子集:

左边指的是前 i-1 个物品(不含第 i 个物品的情况),则最大价值表示为 f[i-1] [j] ;右边指的是含第 i 个物品的情况,为前 i-1 个物品、总体积为 j - v[i] 的情况下的最大价值 + 第 i 个物品的价值 w[i] ,即最大价值表示为 f[i - 1] [j - v[i] ] + w[i]。f[i] [j] 的值就为左右两子集的最大值。
代码实现
import java.util.Scanner;
//二维数组解决01背包问题
public class bag01 {
static int N = 1010;
static int[] v = new int[N];
static int[] w = new int[N];
static int[][] f = new int[N][N];
public static void main(String[] args) {
Scanner myscanner = new Scanner(System.in);
//输入物品个数 n和背包体积 m
int n = myscanner.nextInt();
int m = myscanner.nextInt();
//输入物品体积数组和价值数组
for (int i = 1; i <= n; i++) {
v[i] = myscanner.nextInt();
w[i] = myscanner.nextInt();
}
for (int i = 1; i <= n; i++) { //i=0时f都为0(前0个物品,没东西装)
for (int j = 0; j <= m; j++) {
f[i][j] = f[i - 1][j]; //f=左子集
if (j >= v[i]) { //只有在背包体积 > 第 i个物品体积时才有右子集
f[i][j] = Math.max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
}
System.out.println(f[n][m]);
}
}
优化
二维转化为一维:
删掉了第一维:在前i个物品中取。
f[j]表示:总体积不超过 j 的情况下的最大总价值。
为何能转化为一维?
二维时的更新方式:f [i] [j] = max( f [i - 1] [j] , f [i - 1] [ j - v[i] ] + w[i]);
我们发现,对于每次循环的下一组 i,只会用到 i - 1 来更新当前值,不会用到之前的值。所以在更新某一组 i 的时候就可以考虑把刚刚用过的 i - 1 这一组值给覆盖(即用滚动数组)。具体看下图:

对于二维数组:A就是我们原本每次要更新的 f [i] [j] ;B是我们要用到的 f [i - 1] [j] ;因为 j-v[i] 一定< j ,则 f [i - 1] [ j - v[i] ]一定在B之前,则假设为C。由图显而易见,更新A的话一定要用到C点,那么覆盖数组的时候就一定要从每行的最后往前覆盖(即从m到0)。因为如果从前往后就会先把C覆盖了,A要用的时候就不是原来的C。
如何转化为一维呢?
对于拿前 i 个物品背包容量为 j 的情况:
- 如果第 i 个不拿的话(即左子集),和前一位置的信息(原来i-1数组的这个位置上的值)是相同的,即A=B,原先的B就是这里要的A,所以可以直接删除原先的 f[i] [j] = f [i - 1] [j] 这一行代码。
- 如果第 i 个拿的话,则A = max(B, C)。所以,更新方式就为:f[j]=max(f[j], f[ j - v[i] ] + w[i]);
- j 到 v[i] 就可以了,因为 j 再小的话就装不下第 i 个物品,再往前的值就和原数组的值一样了。(即上1)
//一维数组解决01背包问题(只显示需修改之处)
static int[] f = new int[N];
for (int i = 1; i <= n; i++) {
for (int j = m; j >= v[i]; j--)
f[j] = Math.max(f[j], f[j - v[i]] + w[i]);
}
System.out.println(f[m]);
完全背包
问题描述
和01背包的区别:每个物品不限装入背包的次数
思路分析
f [i] [j] 集合划分为若干子集:装0个第 i 个物品,装1个第 i 个物品…装k个第 i 个物品。如下图:

装0个第 i 个物品的情况就是 f [i - 1] [j] ;装k个第 i 个物品就是 f [ i-1 ] [ j - k * v[i] ] + k * w[i]。我们发现,当k=0时正好也满足前者,所以可以合并。使得对于每一个物品 i ,从小到大的 j ,遍历 k,不断更新最大值。
代码实现
//只显示与01背包二维实现不同的部分
for (int i = 1; i <= n; i++) { //i=0时f都为0(前0个物品,没东西装)
for (int j = 0; j <= m; j++) {
for (int k = 0; k*v[i] <= j ; k++) {
f[i][j] = Math.max(f[i][j], f[i - 1][j - k*v[i]] + k*w[i]);
}
}
}
优化1
因为走三重循环,导致运行效率不高,考虑优化。
根据之前的子集划分,得到 1 式。令 1 式的 j = j-v ,可得 2 式。观察得知,除了第一项,1式与2式的每一项都刚好差一个w[i] ,因此 f [i] [j] 可以改写为
for (int i = 1; i <= n; i++) { //i=0时f都为0(前0个物品,没东西装)
for (int j = 0; j <= m; j++) {
f[i][j] = f[i - 1][j];
if(j >= v[i])
f[i][j] = Math.max(f[i][j], f[i][j - v[i]] + w[i]);
}
}
优化2
如1.4节,优化为一维数组表示。注意:此处的 j 要从小到大循环了,因为如下图:
1式为01背包问题,用的是 i-1 这一行的数据,即求A需要BC两点;而完全背包根据2式,求A点是需要BD两点,所以应该先更新D点再求A,即 j 应该从小到大遍历。
for (int i = 1; i <= n; i++) { //i=0时f都为0(前0个物品,没东西装)
for (int j = v[i]; j <= m; j++)
f[j] = Math.max(f[j], f[j - v[i]] + w[i]);
}
多重背包
问题描述
第 i 个物品最多装入背包的次数为 s[i]
思路分析
就把完全背包的k当成 s[i] 就行
代码实现
暴力解法:
- 定义数组s:
static int[] s = new int[N];
- 输入s:
for (int i = 1; i <= n; i++) {
v[i] = myscanner.nextInt();
w[i] = myscanner.nextInt();
s[i] = myscanner.nextInt();
}
- 修改k跳出循环的条件:
k*v[i] <= j && k <=s[i]
优化
当物品种类N和背包容量V较大时,采用以上暴力解法可能会超时,这时就要考虑优化。像2.4的优化思路写出式子后发现, 2式会比1式最后多一项,所以2.4的优化思路不能拿过来。 参考二进制,想到一种优化思路:
若第 i 个物品允许装入的最大个数为 s,则不一定要按装1个、装2个…装s个来枚举。先考虑特殊的 s=1023,根据二进制算法,可以考虑将1个第 i 个物品、2个第 i 个物品、4个第 i 个物品…2^k个第 i 个物品打包成一件大物品,然后就等价于01背包问题。
为什么等价01背包问题?
对于每一个第 i 个物品,都是一个01背包问题:第1个物品的体积v为v[ i ],价值为w[ i ];第2个物品的体积v为2v[ i ],价值为2w[ i ];第3个物品的体积v为4v[ i ],价值为4w[ i ]…第 k个物品的体积v为kv[ i ],价值为kw[ i ]。根据二进制可知,每个物品现在都是要么取要么不取,取的话只能取一次,不就是01背包问题。现在就和这个s没有关系了。对于所有物品,则是若干个01背包问题的集合,也是01背包问题。
为什么可以等价成01背包问题(为什么可以用二进制优化)?
因为二进制可以表示所有数。当按上述思路打包物品时,可以保证第 i 个物品拿0~s[i] 件。
具体打包方式
先考虑特殊的 s=1023:打包成1个物品、2个物品、4个物品…512个物品。根据等比数列求和(前k项和为2^(k+1) -1,打包完之后组合,就可以满足装 0~1023(1+2+4+…512)个物品。
对于一般的s=200,则打包成1个物品、2个物品、4个物品…64个物品(前面求和为127,下一个大件物品若为128就>200了),所以最后一个大件物品=73个物品。因此打包大件的原则如下:,其中
。前k项满足取到的物品个数为
,加上c则为
。那我们需要考虑,是否两个区间中间会有空隙(即取不到的个数),如下图所示:
但因为
,所以可以保证B点在A点的位置或其左边,即两个区间衔接得上。
代码实现
import java.util.Scanner;
class Main{
static int N = 25000;
static int[] v = new int[N];
static int[] w = new int[N];
static int[] f = new int[N];
public static void main(String[] args) {
Scanner myscanner = new Scanner(System.in);
//输入物品个数 n和背包体积 m
int n = myscanner.nextInt();
int m = myscanner.nextInt();
int cnt = 0; //记录转换成01背包问题后的体积数组v的下标
//输入第i个物品体积、价值、允许装入的最大个数
for (int i = 1; i <= n; i++) {
int vi, wi, si;
vi = myscanner.nextInt();
wi = myscanner.nextInt();
si = myscanner.nextInt();
int k = 1; //打包的大件物品中含第i个物品的个数
while (k <= si) {
cnt++;
v[cnt] = vi * k; //转换成01背包问题后第cnt个物品的v
w[cnt] = wi * k; //转换成01背包问题后第cnt个物品的w
si -= k;
k *= 2;
}
if (si > 0) { //此时的si就是最后一件大包物品的第i个物品的个数
cnt++;
v[cnt] = vi * si;
w[cnt] = wi * si;
}
} //循环完就得到v和w数组,就和01背包问题一毛一样
n = cnt;
for (int i = 1; i <= n; i++) { //i=0时f都为0(前0个物品,没东西装)
for (int j = m; j >= v[i]; j--)
f[j] = Math.max(f[j], f[j - v[i]] + w[i]);
}
System.out.println(f[m]);
}
}
分组背包
问题描述
特殊性:每组物品有若干个,**同一组内的物品最多只能选一个
思路分析

对于前 i 组的情况 f [i] [j] ,分为第 i 组的物品根本不要——f [ i-1 ] [j] ;和要第 i 组的第 k 个的情况——(如上)
代码实现
import java.util.Scanner;
class Main{
static int N = 1110;
static int[][] v = new int[N][N];
static int[][] w = new int[N][N];
static int[] f = new int[N];
static int[] s = new int[N];
public static void main(String[] args) {
Scanner myscanner = new Scanner(System.in);
//输入物品个数 n和背包体积 m
int n = myscanner.nextInt();
int m = myscanner.nextInt();
//输入物品体积和价值
for (int i = 1; i <= n; i++) {
s[i] = myscanner.nextInt(); //第i个分组所含物品种类数
for (int j = 0; j < s[i]; j++) {
v[i][j] = myscanner.nextInt();
w[i][j] = myscanner.nextInt();
}
}
for (int i = 1; i <= n; i++)
for (int j = m; j >= 0; j--)
for (int k = 0; k < s[i]; k++)
if(v[i][k] <= j)
f[j] = Math.max(f[j], f[j - v[i][k]] + w[i][k]);
System.out.println(f[m]);
}
}