普通的背包模型
f[i][j] 表示从前i个物品选,体积不大于j的最大价值
01背包
int f[N][M];
int dp(){
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];
if(j>=w[i]){
f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
}
}
}
return f[n][m];
}
int f[M];
int dp1(){
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
return f[m];
}
完全背包
完全背包用同层状态更新,压缩到一维时正序更新
int f[N][M];
int dp(){
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];
if(j>=w[i]){
f[i][j]=max(f[i][j],f[i][j-w[i]]+v[i]);
}
}
}
return f[n][m];
}
int f[M];
int dp1(){
for(int i=1;i<=n;i++){
for(int j=w[i];j<=m;j++){
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
return f[m];
}
多重背包
int f[N][M];
int dp(){
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];
for(int k=1;k<=s[i];k++){
if(j>=k*w[i]){
f[i][j]=max(f[i][j],f[i-1][j-k*w[i]]+k*v[i]);
}
}
}
}
return f[n][m];
}
int f[M];
int dp1(){
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){
for(int k=1;k<=s[i];k++){
if(j>=k*w[i]){
f[j]=max(f[j],f[j-k*w[i]]+k*v[i]);
}
}
}
}
return f[m];
}
int f[M];
void init(){
int cnt=0;
for(int i=1;i<=n;i++){
int a,b,s;cin>>a>>b>>s;
int k=1;
while(s>=k){
cnt++;
w[cnt]=k*a;
v[cnt]=k*b;
s-=k;
k*=2;
}
if(s>0){
cnt++;
w[cnt]=s*a;
v[cnt]=s*b;
}
}
n=cnt;
}
int dp(){
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
return f[m];
}
分组背包 (每组最多选一个)
int f[N][M];
int dp(){
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){//注意要先枚举体积
f[i][j]=f[i-1][j];
for(int k=1;k<=s[i];k++){//最后枚举组内物品
if(j>=w[i][k])
f[i][j]=max(f[i][j],f[i-1][j-w[i][k]]+v[i][k]);
}
}
}
return f[n][m];
}
int f[M];
int dp1(){
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){//注意要先枚举体积,枚举到0
for(int k=1;k<=s[i];k++){//最后枚举组内物品
if(j>=w[i][k])
f[j]=max(f[j],f[j-w[i][k]]+v[i][k]);
}
}
}
return f[m];
}
二维费用的01背包
int f[M1][M2];
int dp(){
for(int i=1;i<=n;i++){
for(int j=m1;j>=w1[i];j--){
for(int k=m2;k>=w2[i];k--){
f[j][k]=max(f[j][k],f[j-w1[i]][k-w2[i]]+v[i]);
}
}
}
return f[m1][m2];
}
注意:在背包问题中,需要根据题意确定循环条件是否有等号,例如本题中第二维费用要求大于0,所以不能带等号
int v1[N],v2[N];
int f[M1][M2];
int dp(){
for(int i=1;i<=n;i++){
for(int j=m1;j>=v1[i];j--){
for(int k=m2;k>v2[i];k--){
f[j][k]=max(f[j][k],f[j-v1[i]][k-v2[i]]+1);
}
}
}
return f[m1][m2];
}
如果要找满足最大价值时,消耗某费用最少的状态,从后向前循环查找即可
int i;
for(i=m2;i>0;i--){
if(f[m1][i]!=f[m1][m2]){
break;
}
}
cout<<m2-i;
方案数 01背包
题意:n个正整数,求组合成m的方案数
思路:每个正整数看作一个物品,体积为正整数的值
f [i][j] 表示从前i个选,体积恰好为j的方案数
注意:求方案数时要将初始状态设置为1 ,即 f[0]=1
int f[M];
int dp(){
f[0]=1;
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
f[j]+=f[j-v[i]];
}
}
return f[m];
}
方案数 完全背包
求方案数的转移方式与相应的背包转移方式相同
int dp(){
f[0]=1;
for(int i=1;i<=n;i++){
for(int j=v[i];j<=m;j++){
f[j]+=f[j-v[i]];
}
}
return f[m];
}
题意:求不能由其他数组成的数的个数
思路:求方案数,方案数为1的说明只能由自己组成
int dp(){
memset(f,0,sizeof f);
f[0]=1;
for(int i=1;i<=n;i++){
for(int j=v[i];j<=m;j++){//m为v[i]的最大值
f[j]+=f[j-v[i]];
}
}
int cnt=0;
for(int i=1;i<=n;i++){
//f[i]==1 表示i只能由i自己组成
if(f[v[i]]==1) cnt++;
}
return cnt;
}
混合背包
根据物品所属的背包类别,依次处理每个物品。当该物品属于01背包时,可以看作次数为1的多重背包处理
int ww[N],vv[N];
int f[M];
int dp(){
for(int i=1;i<=n;i++){
int w,v,s;cin>>w>>v>>s;
//完全背包
if(!s){
for(int j=w;j<=m;j++){
f[j]=max(f[j],f[j-w]+v);
}
}else{
//01背包可以看作是次数为1的多重背包
if(s==-1) s=1;
//多重背包
int cnt=0;
int k=1;
while(k<=s){
++cnt;
ww[cnt]=k*w;
vv[cnt]=k*v;
s-=k;
k*=2;
}
if(s>0){
++cnt;
ww[cnt]=s*w;
vv[cnt]=s*v;
}
for(int j=1;j<=cnt;j++){
for(int k=m;k>=ww[j];k--){
f[k]=max(f[k],f[k-ww[j]]+vv[j]);
}
}
}
}
return f[m];
}
不同背包问题的初始化 (※)
需要注意“初始化”和“转移条件”两个方面
二维费用背包的“至少”问题
一般的背包问题求的是费用不多于m的情况下,获得价值的最大值,而本题中询问的是 第一维费用不小于m1,第二维费用不小于m2的情况下,获得价值的最小值
①首先,由于求的是价值的最小值,我们需要将状态初始化为正无穷,且初始状态为0,即f[0]=0
②注意费用的条件是“不小于”,当费用为负数k时,表示费用不小于k的情况,依然符合条件(区别于一般背包费用的“不大于”条件,不存在费用<=负数的情况)。
③对费用至少需要j的f[j],如果物品费用k大于j,此时k满足不小于费用j的条件,取完物品后,转移到f[0],表示原来至少需要的费用j已经被满足,已经不再需要费用,即至少不小于0的情况
int dp(){
memset(f,0x3f,sizeof f);
f[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=m1;j>=0;j--){
for(int k=m2;k>=0;k--){
f[j][k]=min(f[j][k],f[max(0,j-w1[i])][max(0,k-w2[i])]+v[i]);
}
}
}
return f[m1][m2];
}
输出背包方案
题意:输出分组背包的方案
思路:从最终状态的费用开始,根据转移方程,在本题中是从最后一组开始,枚举选择的物品个数,来找到该组的方案,同时更新费用,再寻找前一组的方案
int v[N][M];
int f[N][M];
int dp(){
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];
for(int k=1;k<=j;k++){
f[i][j]=max(f[i][j],f[i-1][j-k]+v[i][k]);
}
}
}
return f[n][m];
}
int res[N];
void print(){
int j=m;
for(int i=n;i>=1;i--){
for(int k=0;k<=m;k++){
if(j>=k&&f[i][j]==f[i-1][j-k]+v[i][k]){
res[i]=k;
j-=k;
break;
}
}
}
for(int i=1;i<=n;i++){
cout<<i<<" "<<res[i]<<endl;
}
}
01背包输出方案
题目要求输出最小字典序的方案,需要倒序DP,正序推方案(与一般情况正好相反)
//要求输出方案字典最小序
//倒序dp,正序推方案
void dp(){
for(int i=n;i>=1;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+1][j-v[i]]+w[i]);
}
}
}
void print(){
int j=m;
for(int i=1;i<=n;i++){
if(j>=v[i]&&f[i][j]==f[i+1][j-v[i]]+w[i]){
cout<<i<<" ";
j-=v[i];
}
}
}
有依赖的背包问题
题意:物品的关系构成一颗树,要想选某个物品,必须选上该物品的父物品
思路:可以当作是一个分组背包,每组中包含的是若干颗子树,对应若干个物品。对于组内的每个物品,先枚举可以给物品的体积j(预留出父物品的体积),然后枚举子物品要占的体积,最后对父物品选择。
int f[N][N];
//f[i][j] 以i为根节点的子树,体积不大于j的最大价值
void dfs(int u){
//相当于分组背包
//遍历该组的所有物品
for(int i=h[u];~i;i=ne[i]){
int son=e[i];
dfs(son);
//对该物品进行选择,物品其实是一颗子树
//枚举可以给子物品的体积(预留出父物品本身)
for(int j=m-w[u];j>=0;j--){
//枚举子物品要占的体积
//f[son][k] 以son为根节点的子树,体积不大于k的最大价值
for(int k=0;k<=j;k++){
f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]);
}
}
}
//将父物品选上
for(int j=m;j>=w[u];j--){
f[u][j]=f[u][j-w[u]]+v[u];
}
//如果不能选父物品,则不能选任何物品
for(int j=0;j<w[u];j++){
f[u][j]=0;
}
}
//dfs(root);
//cout<<f[root][m];
题意:物品分主件和附件,选择附件必须选择所属的主件
思路:对所有的主件进行遍历,在枚举体积时,对每种主件+附件的方案进行枚举选择
PII ms[N];//主件
vector<PII> sv[N];//附件
int f[M];
int dp(){
for(int i=1;i<=n;i++){
if(ms[i].v){//主件
for(int j=m;j>=0;j--){
//枚举所有选择附件的情况
for(int k=0;k<1<<sv[i].size();k++){
//主件+附件的体积和价值
int v=ms[i].v,w=ms[i].w;
for(int z=0;z<sv[i].size();z++){
if(k>>z&1){
v+=sv[i][z].v;
w+=sv[i][z].w;
}
}
if(j>=w){
f[j]=max(f[j],f[j-w]+v);
}
}
}
}
}
return f[m];
}
背包最优解的方案数
思路:①在进行转移时,如果转移前后的价值相同,则对方案数进行累加
②最后将等于最大价值的方案数量进行累加即可
int f[N],g[N];
int dp(){
g[0]=1;
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];j--){
int maxn=max(f[j],f[j-w[i]]+v[i]);
int cnt=0;
if(f[j]==maxn) cnt+=g[j];
if(f[j-w[i]]+v[i]==maxn) cnt+=g[j-w[i]];
g[j]=cnt%mod;
f[j]=maxn;
}
}
int res=0;
for(int j=m;j>=0;j--){
if(f[j]==f[m]){
res=(res+g[j])%mod;
}else break;
}
return res;
}
贪心决策01背包
题意:每个物品的费用是时间,价值会随着时间而减少
思路:采用贪心决策,按一定的规律对所有的物品进行排序
struct stone{
int s;//体积
int e;//原始价值
int l;//每秒失去l价值
//贪心,根据s/l进行排序
bool operator<(const stone& t) const{
return s*1.0/l<t.s*1.0/t.l;
}
}st[N];
int dp(){
memset(f,0,sizeof f);
for(int i=1;i<=n;i++){
for(int j=m;j>=st[i].s;j--){
f[j]=max(f[j],f[j-st[i].s]+st[i].e-(j-st[i].s)*st[i].l);
}
}
int res=0;
for(int i=1;i<=m;i++){
res=max(res,f[i]);
}
return res;
}
逆向思维(代价与价值互换)
正常背包会超时,把代价与价值互换,f[i]表示拿到i金币花的最少时间
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5,M=3e4+5;
int f[M];//拿到i金币的最少时间
int n,m;
int w[N],v[N];
int main(){
cin>>n>>m;
memset(f,0x3f,sizeof f);
f[0]=0;
for(int i=1;i<=n;i++) cin>>w[i];
for(int i=1;i<=n;i++) cin>>v[i];
for(int i=1;i<=n;i++)
for(int j=3e4;j>=v[i];j--)
f[j]=min(f[j],f[j-v[i]]+w[i]);
int res=0;
for(int i=0;i<=3e4;i++)
if(f[i]<=m) res=i;
cout<<res;
}
题记
整数拆分
完全背包求方案数,一个数可以由若干个2的次幂组成,将2的次幂作为物品
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=1e6+5,mod=1e9;
ll f[N];
//w[i] 2^i
//完全背包
int w[N],cnt;
int n;
void init(){
for(ll d=1;d<=n;d*=2){
w[++cnt]=d;
}
}
int main(){
cin>>n;
init();
f[0]=1;
for(int i=1;i<=cnt;i++){
for(int j=w[i];j<=n;j++){
f[j]=(f[j]+f[j-w[i]])%mod;
}
}
cout<<f[n];
}
包子凑数
结论:由若干个数组合的数一定是这些数的gcd的倍数,所以如果gcd不等于1,则不能组合的数有无限个,剩下的就是完全背包
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=1e4+5;
bool f[N];
int n;
int w[N];
int gcd(int a,int b){
return b?gcd(b,a%b):a;
}
int main(){
cin>>n;
int all_gcd=w[1];
for(int i=1;i<=n;i++){
cin>>w[i];
all_gcd=gcd(all_gcd,w[i]);
}
//组合的数一定是全部数gcd的倍数
if(all_gcd!=1){//gcd!=1说明有无限数不能被组合
cout<<"INF";
return 0;
}
f[0]=true;
for(int i=1;i<=n;i++){
for(int j=w[i];j<N;j++){
f[j]|=f[j-w[i]];
}
}
int res=0;
for(int i=1;i<N;i++){
if(!f[i]){
res++;
}
}
cout<<res;
}
波动数列
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=1e3+5,mod=1e8+7;
ll f[N][N];
int n,s,a,b;
//f[i][j] 加i项 和模n的余数为j 的方案数
int get_mod(int a,int b){
return ((a%b)+b)%b;
}
int main(){
cin>>n>>s>>a>>b;
f[0][0]=1;
for(int i=1;i<n;i++){
for(int j=0;j<n;j++){
//第i个选a 保持第i-1时模n的余数 加上(n-i)*a后模n的余数 为j
f[i][j]=(f[i][j]+f[i-1][get_mod(j-(n-i)*a,n)])%mod;
//第i个选-b 保持第i-1时模n的余数 减去(n-i)*b后模n的余数 为j
f[i][j]=(f[i][j]+f[i-1][get_mod(j+(n-i)*b,n)])%mod;
}
}
//序列长度为n 需要加n-1次 最终和的余数为s%n
cout<<f[n-1][get_mod(s,n)];
}