1)01背包
1:二维数组
- 非常熟悉和基础,没什么可讲的
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
// 解题思路:
const int N=1e3+5;
const int M=1e3+5;
int n,m;
int v[N],w[N];
int f[N][M];
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++) {
scanf("%d%d",&v[i],&w[i]); // v:重量,w:价值
}
// 枚举物品
for(int i=1;i<=n;i++) {
// 枚举背包容量
for(int j=0;j<=m;j++) {
f[i][j]=f[i-1][j];
// 如果能拿物品i
if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]); // v:体积,w:价值
}
}
int res=0;
// 当取n个物品时最大总价值,其实也就是f[n][m]
for(int i=0;i<=m;i++) res=max(res,f[n][i]);
cout<<res<<endl;
cout<<f[n][m]<<endl;
return 0;
}
2:一维数组
- 因为 f [ i ] [ j ] f[i][j] f[i][j] 只与 f [ i − 1 ] [ j ] f[i-1][j] f[i−1][j] 、 f [ i − 1 ] [ j − v [ i ] ] + w [ i ] f[i-1][j-v[i]]+w[i] f[i−1][j−v[i]]+w[i] 有关,即只与上一个状态(上一次选择)有关,那么我们只需要开一个一维数组记录上一次选择即可,这个数组名为滚动数组
- 由于滚动数组已经表示了上一次选择的状态,所以代码 f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j]=f[i-1][j] f[i][j]=f[i−1][j] 可以去掉
- 枚举背包体积时必须从大到小,原因已经在代码中解释,如果还是 f o r ( i n t j = 0 ; j < = m ; j + + ) for(int\ j=0;j<=m;j++) for(int j=0;j<=m;j++) ,那么用到的实际上还是 f [ i ] f[i] f[i] 这个状态而不是 f [ i − 1 ] f[i-1] f[i−1] 这个状态
- 如果把所有的 f [ i ] f[i] f[i] 都初始化为 0 0 0,那么 f [ m ] f[m] f[m] 表示的是当背包体积 < = m <=m <=m 时的最大价值是多少;如果只把 f [ 0 ] f[0] f[0] 初始化为 0 0 0 ,而其他初始化为 − I N F -INF −INF,那么 f [ m ] f[m] f[m] 表示的是体积刚好等于 m m m 时的最大价值,所以全部初始化为 0 0 0 的话 f [ m ] f[m] f[m] 就是答案
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
// 解题思路:
const int N=1e3+5; // 最大物品数
const int M=1e3+5; // 最大容量
int n,m;
int f[M]; // f[i]:物品容量为i时的背包最大容量
int v[N],w[N];
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++) {
scanf("%d%d",&v[i],&w[i]);
}
for(int i=1;i<=n;i++) {
// 因为f[i][j]只与f[i-1][j]/f[i-1][j-v[i]]+w[i],只与上一个状态有关
// 所以二维数组中有很多空间被浪费,计算完就可以丢掉了
// 所以用滚动数组,但是在枚举背包体积的时候必须从大到小排序,若顺序的话再用到上一个状态时就已经被更新过了
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
int res=0;
for(int i=0;i<=m;i++) res=max(res,f[i]);
cout<<res<<endl;
cout<<f[m]<<endl;
return 0;
}
2)完全背包
1:朴素做法
- 时间复杂度和空间复杂度都较高,手动枚举每个物品可选择数量
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
// 解题思路:
const int N=1e3+5;
const int M=1e3+5;
int f[N][M];
int n,m;
int v[N],w[N];
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++) {
scanf("%d%d",&v[i],&w[i]);
}
// 朴素做法,枚举物品
for(int i=1;i<=n;i++) {
// 枚举背包容量
for(int j=0;j<=m;j++) {
// 枚举物品可挑选数量
for(int k=0;k*v[i]<=j;k++) {
f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
}
}
}
cout<<f[n][m]<<endl;
return 0;
}
2:公式优化
- f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w , f [ i − 1 , j − 2 v ] + 2 w , f [ i − 1 , j − 3 v ] + 3 v , . . . ) f[i,j]=max(f[i-1,j],f[i-1,j-v]+w,\ f[i-1,j-2v]+2w,\ f[i-1,j-3v]+3v,\ ...) f[i,j]=max(f[i−1,j],f[i−1,j−v]+w, f[i−1,j−2v]+2w, f[i−1,j−3v]+3v, ...),① f [ i , j − v ] = m a x ( f [ i − 1 , j − v ] , f [ i − 1 , j − 2 v ] + w , f [ i − 1 , j − 3 v ] + 2 w , f [ i − 1 , j − 4 v ] + 3 w , . . . ) f[i,j-v]=max(f[i-1,j-v],f[i-1,j-2v]+w,\ f[i-1,j-3v]+2w,\ f[i-1,j-4v]+3w,\ ...) f[i,j−v]=max(f[i−1,j−v],f[i−1,j−2v]+w, f[i−1,j−3v]+2w, f[i−1,j−4v]+3w, ...)②,
- 可以看出②式是①式第二项起 + w +w +w 的结果,所以 f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i , j − v ] + w ) f[i,j]=max(f[i-1,j],\ f[i,j-v]+w) f[i,j]=max(f[i−1,j], f[i,j−v]+w),即转移时与上一个状态无关,所以我们就可以顺序枚举物品容量
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
// 解题思路:
const int N=1e3+5;
const int M=1e3+5;
int n,m;
int v[N],w[N];
int f[N][M];
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++) {
scanf("%d%d",&v[i],&w[i]);
}
for(int i=1;i<=n;i++) {
for(int j=0;j<=m;j++) {
f[i][j]=f[i-1][j];
// 至少能装下一个
if(j>=v[i]) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]); // 和01背包的区别就在于可以与当前状态有关
}
}
cout<<f[n][m]<<endl;
return 0;
}
3:再优化一维数组
- 注意注释中是如何分析当 f f f 数组是一维时状态转移的是当前状态还是上一次的状态
- 因为滚动数组本身代表上一次的状态,所以 f [ i , j ] = f [ i − 1 , j ] f[i,j]=f[i-1,j] f[i,j]=f[i−1,j] 直接优化成 f [ j ] = f [ j ] f[j]=f[j] f[j]=f[j],无意义,所以不用写
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
// 解题思路:
const int N=1e3+5;
const int M=1e3+5;
int n,m;
int v[N],w[N];
int f[M];
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++) {
scanf("%d%d",&v[i],&w[i]);
}
for(int i=1;i<=n;i++) {
// f[i][j]=f[i-1][j]直接删掉一维,即f[j]=f[j](一维滚动数组直接代表上一次的状态,所以不需要这句代码)
// j是从小到大枚举的
for(int j=v[i];j<=m;j++) {
// j-v[i]是<j的,所以算f[j]的时候f[j-v[i]]已经被算过了,所以是第i层的f[i][j-v[i]]
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[m]<<endl;
return 0;
}
3)多重背包
1:朴素做法
- 类似于完全背包的朴素做法,直接第三层去枚举物品的个数,不过多了一个限制条件, k < = s [ i ] k<=s[i] k<=s[i]
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
// 解题思路:
const int N=1e2+5;
const int M=1e2+5;
int n,m;
int v[N],w[N],s[N];
int f[N][M];
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++) {
scanf("%d%d%d",&v[i],&w[i],&s[i]);
}
// 枚举物品
for(int i=1;i<=n;i++) {
for(int j=0;j<=m;j++) {
// 能拿的个数从0~s[i],且k*v[i]能被背包装下
for(int k=0;k<=s[i] && k*v[i]<=j;k++) {
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
}
cout<<f[n][m]<<endl;
return 0;
}
2:二进制优化
-
如果按照类似于完全背包从状态转移方程式入手的角度来优化是不可行的,因为完全背包问题不会多最后一项出来,多了一个 i f if if 的判定条件,最后一项会因为超出了 j j j 而被舍去
-
如何优化呢?若 s = 1023 s=1023 s=1023,那么 1023 1023 1023 能由其 s u m ( 2 n ) < = 1023 sum(2^n)<=1023 sum(2n)<=1023 的这 n n n 个 2 2 2 的指数组成( n = 9 n=9 n=9),把 1023 1023 1023 个物品当作是 1023 1023 1023 次 01 01 01 背包,再通过二进制优化的方式把从枚举 1023 1023 1023 次转换为只需要枚举 9 9 9 次(倍增法思想)
-
比如 s = 200 s=200 s=200, ∑ i = 1 n 2 i < = 200 \sum_{i=1}^n\ 2^i<=200 ∑i=1n 2i<=200 可得 { 1 , 2 , 4 , 8 , 16 , 32 , 64 } \{1,\ 2,\ 4,\ 8,\ 16,\ 32,\ 64\} {1, 2, 4, 8, 16, 32, 64},这几个数字之和为 127 127 127,则只需要再来一个 73 73 73 就可以凑出 [ 0 , 200 ] [0,200] [0,200] 的数字;即对于任意一个 s s s ,我们可以凑成 { 1 , 2 , 4 , 8 , . . . , 2 k , c } \{1,\ 2,\ 4,\ 8,\ ...,\ 2^k,\ c\} {1, 2, 4, 8, ..., 2k, c},其中 c < 2 k + 1 c<2^{k+1} c<2k+1
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
// 解题思路:
const int N=2e4+5; // 1000*log(2000),2w4左右
const int M=2e3+5;
int n,m;
int v[N],w[N];
int f[N];
int main() {
cin>>n>>m;
int cnt=0;
for(int i=1;i<=n;i++) {
int a,b,s;
scanf("%d%d%d",&a,&b,&s);
int k=1; // 从2^0开始,每组初始物品数
while(k<=s) {
// 每次把k个第i个物品打包在一起
cnt++; // 编号++
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
// 补常数
if(s>0) {
cnt++; // 第i个物品的最后一次分组
v[cnt]=a*s;
w[cnt]=b*s;
}
}
n=cnt; // 物品个数变成总的打包数
for(int i=1;i<=n;i++) {
// 至少能拿一个物品,所以j>=v[i]
// 从大到小枚举
for(int j=m;j>=v[i];j--) {
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[m]<<endl;
return 0;
}
3:单调队列优化
- 看不懂,溜了溜了