背包DP
返回母本《动态规划》
动态规划常用模型——背包问题:
给定一组物品,每种物品都有自己的重量和价格,在限定的总重量和一定选择规则内,我们如何选择,才能使得物品的总价格最高。
1.01背包问题
-
问题描述:
给定一个容量为
-
输入格式
第一行两个整数,
接下来有
-
输出格式
输出一个整数,表示最大价值。
-
数据范围
-
输入样例
4 5
1 2
2 4
3 4
4 5
-
输出样例:
8
-
解:
我们设
(此时,“集合”:选择前
对于计算
“不选”:从前
i-1 个物品、使用最大体积为j 的选法中最大价值选法的所得价值,即f(i-1,j) ;“选”:显然,选了当前物品后,体积为
j-v_i ,则在选当前物品前,状态f(i-1,j-v_i) ,在加上当前物品,即f(i-1,j-v_i)+w_i 。
得状态转移方程:
f(i,j)=\max(f(i-1,j),f(i-1,j-v_i)+w_i)
-
代码1.0:
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m;//物品数量、背包容积
int v[N],w[N];//物品体积、物品价值
int f[N][N];//状态或答案
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d",&v[i],&w[i]);
//朴实输入
//f[0][0~m]=0
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-1][j-v[i]]+w[i]);
}
printf("%d",f[n][m]);
return 0;
}
-
代码优化2.0:
可以发现,无论是代码还是方程中,f[i][j] 中 i 这一层在每次更新中只用到了
然而还发现,在代码和方程中,
还有一个问题是,在原代码中是从体积小到体积大来“规划”,事实上前一个
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m;//物品数量、背包容积
int v[N],w[N];//物品体积、物品价值
int f[N];//状态或答案
int main(){
scanf("%d%d",&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=m;j>=v[i];j--)//倒着规划
f[j]=max(f[j],f[j-v[i]]+w[i]);
//不选;选,前提是装得下
printf("%d",f[m]);
return 0;
}
其实,抛开背包问题本身来讲,以线性DP的视角来看,
所谓的“体积”是一个限制元素,而“价值”是一个目标元素
,这样理解,有助于做题时的阅读理解。
-
练习
-
luogu P1048 [NOIP2005 普及组] 采药
-
luogu P2871 [USACO07DEC] Charm Bracelet S
-
luogu P1060 [NOIP2006 普及组] 开心的金明
attention:简单的转化 -
luogu P1049 [NOIP2001 普及组] 装箱问题
attention:貌似题目中并没有价值,但一般来讲,目标元素即为“价值”,限制元素为“体积”,所以,你的结论是? -
luogu P2639 [USACO09OCT] Bessie's Weight Problem G
attention:你做过,不是吗?同时,二倍经验。 -
AcWing 278.数字组合
attention:出现了一个DP核心的重大转变,好好消化。
2.完全背包问题
问题描述:给定一个容量为
-
例题:
有
N 种物品和一个容量是V 的背包,每种物品都有无限件可用。 第i 种物品的体积是v_i ,价值是w_i 。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
-
输入格式
第一行两个整数,
接下来有
-
输出格式
输出一个整数,表示最大价值。
-
数据范围
-
输入样例
4 5
1 2
2 4
3 4
4 5
-
输出样例:
10
-
解:
与01背包类似地,只是在取个数上多了一维,你以为有无限个,实际上拿走的
-
初代代码(朴素):
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m;//物品数量、背包容积
int v[N],w[N];//物品体积、物品价值
int f[N][N];//状态或答案
int main(){
scanf("%d%d",&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 z=1;z*v[i]<=j;z++)//实际上个数满足vi*k<=j
f[i][j]=max(f[i-1][j],f[i-1][j-z*v[i]]+z*w[i]);
printf("%d",f[n][m]);
return 0;
}
-
代码优化2.0:
显然,有着与01背包类似的优化方向,但此时只优化了空间,时间复杂度仍然不够优秀。
观察方程
发现
f(i,j)=\max(f(i-1,j),f(i,j-v_i)+w)
得到全新的状态转移方程,再加上空间降维
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m;//物品数量、背包容积
int v[N],w[N];//物品体积、物品价值
int f[N];//状态或答案
int main(){
scanf("%d%d",&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=v[i];j<=m;j++){
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
printf("%d",f[m]);
return 0;
}
-
练习
-
AcWing 1023,买书
attention:涉及一个DP核心的转变,好好消化 -
luogu P5020 [NOIP2018 提高组] 货币系统
attention:这是一道综合性很高的题,首先你得做阅读理解,其次你得有一个转化方向,最后严谨地证明你的结论(当然你可以赌你是对的),还有一点是它涉及到DP核心的一个转变,最后,玩儿得开心《蒟蒻作者的代码及思路》
3.多重背包问题
问题描述:给定一个容量为
-
例题:
有
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
-
输入格式
第一行两个整数,
接下来有
-
输出格式
输出一个整数,表示最大价值。
-
数据范围
-
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
-
输出样例:
10
-
解:
显然,它与完全背包问题非常相似,我们可以很容易的得到初始的状态转移方程:
f[i,j]=\max(\{f[i-1,j-v[i]\cdot k]+k\cdot w[i]\})(i\in [0,N],k\in [0,s[i]])
此时我们当然可以用朴素方法直接写:
-
初代代码:
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int n,m;//物品数量、背包容积
int v[N],w[N],s[N];//物品体积、物品价值
int f[N][N];//状态或答案
int main(){
scanf("%d%d",&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++)
for(int z=0;z*v[i]<=j && z<=s[i];z++)//实际上个数满足vi*k<=j
f[i][j]=max(f[i][j],f[i-1][j-z*v[i]]+z*w[i]);
printf("%d",f[n][m]);
return 0;
}
显然,三重循环太危险了,当限制范围过大时,容易超时,并且空间上也不理想。
如何优化?
显然,多重背包问题的个数是确定的,完全背包问题虽然类似,但两者的性质上有巨大不同,即后者的个数是动态的,不确定的。
其次,我们试着像完全背包问题一样去优化:
f[i,j]=\max(f[i-1,j],f[i-1,j-v[i]]+w[i],f[i-1,j-2v[i]]+2w[i] ……f[i-1,j-s[i]v[i]]+s[i]w[i]) f[i,j-v[i]]=\max(f[i-1,j-v[i]],f[i-1,j-2v[i]]+w[i],f[i-1,j-3v[i]]+2w[i],……,f[i-1,j-(s[i]+1)v[i]]+(s[i]+1)w[i])
可以看到,
其实,由于物品的个数确定,所以多重背包问题更像01背包问题,我们当然可以把
但这并没有达到优化的目的。
那么,对于任何的
理论可行,实践开始。
#include<bits/stdc++.h>
using namespace std;
const int N=2010;
int n,V;
int v[N],w[N],cnt=0,f[N];
int main(){
scanf("%d%d",&n,&V);
for(int i=1;i<=n;i++){
int a,b,s;
scanf("%d%d%d",&a,&b,&s);
//集成化开始
int j=1;
while(j<=s){//有可能刚好够
cnt++;
v[cnt]=j*a;
w[cnt]=j*b;
s-=j;
j*=2;//倍增
}
if(s>0){//还有剩
cnt++;
v[cnt]=s*a;
w[cnt]=s*b;
}
}
n=cnt;
for(int i=1;i<=n;i++)
for(int j=V;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);//01背包问题
printf("%d",f[V]);
return 0;
}
但是,当
一点想法是,可以注意到,
还记得滑动窗口吗?
那么,这种优化的详解在这儿。蒟蒻不会写
4.分组背包问题
问题描述:给定一个容量为
-
例题:
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。 每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
-
输入格式
第一行有两个整数
接下来有
-
每组数据第一行有一个整数
S_i ,表示第i 个物品组的物品数量; -
每组数据接下来有
S_i 行,每行有两个整数v_{ij},w_{ij} ,用空格隔开,分别表示第i 个物品组的第j 个物品的体积和价值; -
输出格式
输出一个整数,表示最大价值。
-
数据范围
-
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
-
输出样例:
8
-
解:
其实,这跟01背包没啥区别。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int n,m;
int s[N],v[N][N],w[N][N];
int f[N];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&s[i]);
for(int j=1;j<=s[i];j++)
scanf("%d%d",&v[i][j],&w[i][j]);
}
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--){
for(int z=1;z<=s[i];z++){
if(j-v[i][z]>=0)
f[j]=max(f[j],f[j-v[i][z]]+w[i][z]);
}
}
printf("%d",f[m]);
return 0;
}
-
练习:
-
P1757 通天之分组背包
-
P5322 [BJOI2019] 排兵布阵
attenton:阅读理解,技巧性优化,思维与创新。《蒟蒻作者的代码与解析》 -
AcWing 1074.苹果二叉树
可以先到背包训练场练习
5.二维费用背包问题
“二维”
是指对于“限制元素”而言的二维化,当然,二维费用背包问题也包含了上述的01背包、完全背包、多重背包。
二维费用背包问题并不难,接下来,以01背包为例来了解一下。
例题:
-
题目描述
有
每件物品只能用一次。体积是
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。
-
输入格式
第一行三个整数,
接下来有
-
输出格式
输出一个整数,表示最大价值。
-
数据范围
-
输入样例
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
-
输出样例:
8
-
解:
看图示理解。蒟蒻不会?懒得写~逃
当然,优化降维不在话下,同时也要倒序循环。
-
noding:
#include<bits/stdc++.h>
using namespace std;
int n,V,M;
int f[110][110];
int main(){
scanf("%d%d%d",&n,&V,&M);
for(int i=1;i<=n;i++){
int v,m,w;
scanf("%d%d%d",&v,&m,&w);
for(int j=V;j>=v;j--)
for(int k=M;k>=m;k--)
f[j][k]=max(f[j][k],f[j-v][k-m]+w);
}
printf("%d",f[V][M]);
return 0;
}
-
练习:
-
AcWing 1022.宠物小精灵之收服
-
AcWing 1020.潜水员
attention:这里将出现两个特别大的转变,事关DP核心,这个问题难而不难,且很重要,好好消化
6.背包问题求具体方案
相信你一定听说过,有的时候我们可以
……把动态规划中的每个状态看做点,状态间的关系就是有向边,此时动态规划具体化演变成一个有向图……
那么,背包问题更可以说明这个问题
01背包的初态方程:
f[i][j]=\max(f[i-1][j],f[i-1][j-v[i]]+w[i])
那么,常规的背包问题(一维限制,属性MAX,集合“不超过”)可以看成最长路,此时的具体方案,就是最长路的路径。
记录一下就可以了,两种方式:
在动规后,从答案开始,判断转移状态间的相等关系,以此“顺藤摸瓜”。(
\textcolor{blue}{\textbf{费时间}} )在动规时,作对应状态的记录,最后反序查看。(
\textcolor{orange}{{\textbf{又费时间又费空间}}} )
蛋疼的一点是,求具体方案时无法降维优化。
-
例题
-
解
这题唯一的麻烦事是输出字典序最小的方案,
但也不麻烦,由于在找路径时我们是倒序查找,那么在动规时我们倒序规划,令查路径正序即可。
这里就只展示第一种方式。
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N][N];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d",&v[i],&w[i]);
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]);
}
//此时答案变成f[1][m]
int k=m;
for(int i=1;i<=n;i++)
if(k>=v[i] && f[i][k]==f[i+1][k-v[i]]+w[i]){
printf("%d ",i);
k-=v[i];
}
return 0;
}
-
练习
-
P2066 机器分配
attention:注意阅读理解,利用所学《蒟蒻作者的代码及解析》
7.背包问题求最优方案数
特殊的方案数求解。
-
例题
-
解:
其实这一点应和上一点合并,因为他们的理解相同,那么利用上面的思想,对应每个状态设置一个记录表示最优值下的方案数,在转移(或说计算)时,若最优值相等则方案数加一,同时在转移时方案数根据“来路”更新。
具体地,看代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1010,mod=1e9+7;
int f[N],cnt[N];
int main()
{
int n,m;
cin>>n>>m;
cnt[0]=1;
for(int i=1;i<=n;i++)
{
int v,w;
cin>>v>>w;
for(int j=m;j>=v;j--)
{
if(f[j-v]+w>f[j])
{
f[j]=f[j-v]+w;
cnt[j]=cnt[j-v];
}
else if(f[j-v]+w==f[j]) cnt[j]=(cnt[j]+cnt[j-v])%mod;
}
}
int ans=0;
for(int i=0;i<=m;i++)
{
if(f[i]==f[m])ans=(ans+cnt[i])%mod;
}
cout<<ans;
return 0;
}