就是感觉之前 dp 的 blog 太乱了整理一下。
0-1 背包
例题:P1048
朴素算法
思路
对于一个物品,我们可以选,也可以不选。
我们用表示第 i 件物品的重量,
表示第 i 件物品的价值。
考虑表示前 i 件物品放入容量为j的背包中的最大价值。
如果我们放不下第 i 件物品(把它打入冷宫),那么;否则入宫
然后发生了一场宫斗:。
这就是0-1背包的状态转移方程。
Code
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn];
//w[i]表示第i个物品的重量,v[i]是价值
int dp[maxn][maxm];
//dp[i][j]表示前i个物品放入容量为j的背包里的最大值
int main(){
int n,m;
cin>>n>>m;
//n是物品个数,m是背包大小
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
if(j<w[i])//放不下第i个物品
dp[i][j]=dp[i-1][j];//打入冷宫
else//入宫
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);//然后是场宫斗
}
}
cout<<dp[n][m]<<endl;
return 0;
}
滚动数组优化
显然,朴素算法的二维数组太耗费空间了。我们可以进行优化。
观察一下二维数组的代码,注意到 i跟个摆设似的,没啥用途。(注意力惊人)
因为只跟
有关系,所以我们没必要记录 i。
不妨我们来试一试优化(试试就逝世)
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],dp[maxm];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=n;i++){
for(int j=w[i];j<=m;j++)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
cout<<dp[m]<<endl;
return 0;
}
乍一看没什么问题,实则喜提 0 分好成绩。
为什么呢? 更新的比
早!
因此倒过来循环即可。
Code
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],dp[maxm];//w是重量,v是价值
int main(){
int n,m;//n是物品个数,m是背包容量
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=n;i++){
for(int j=m;j>=w[i];i--)//这里注意了:由于如果正着循环,dp[j-w[i]]会比dp[i]更新的早,所以要倒过来循环
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
cout<<dp[m]<<endl;
return 0;
}
完全背包
有N种物品和一个容量是M的背包,每种物品都有无限件可用。
第i种物品的体积是v[i],价值是w[i]。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
洛谷上找不到例题所以只能自己写了。
转 0-1 背包
完全背包和 0-1背包 的差别就是物品可以拿无限次。 所以考虑把完全背包转成 0-1背包。
我们可以想一下,物品有无限件,肯定是超过 M(背包容量)的。
我们可以把每个物品拆分成个小物品。这样就和 0-1 背包一样了。
拆分的代码:
int tot=0;//计算物品总数
for(int i=1;i<=n;i++){
int a,b;//a是重量,b是价值
cin>>a>>b;
for(int j=1;j<=m/a;j++){
w[++tot]=a;
v[tot]=b;
}
}
完整代码
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],dp[maxm];
int main(){
int n,m;
cin>>n>>m;
int tot=0;//计算物品总数
for(int i=1;i<=n;i++){
int a,b;//a是重量,b是价值
cin>>a>>b;
for(int j=1;j<=m/a;j++){
w[++tot]=a;
v[tot]=b;
}
}
for(int i=1;i<=tot;i++){
for(int j=m;j>=w[i];j--)//倒着循环
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
cout<<dp[m]<<endl;
return 0;
}
完全背包自己的算法
这次我们不转 0-1背包。
由于至少放入一个,考虑最后一个放入的物品,其占用 w 的空间,价值是 v。
由于物品有无限多个,放入一个以后还是有无限多个,但是背包大小减少了 w。
问题转换为从前i个物品种选择一些物品放入的背包中可以获得的最大价值。
由此推出状态转移方程:。
我们用滚动数组,优化空间。和 0-1背包类似。
方程优化成这样:。
虽然它的状态转移方程和0-1背包的一样,但是它不用倒着循环,正着循环即可。
#include <bits/stdc++.h>
using namespace std;
//对于物品i,你可以把它打入冷宫(不选)或者让它入宫(选)
//不过啊,这是无限背包,所以可以无限放
//也就是说,你放完一个,还有无限个,但是呢背包的大小减少了w[i]
//因此问题转化为从前i个物品中选一些放入大小为j-w[i]的背包中可获得的最大价值
int w[maxn],v[maxn],dp[maxm];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=n;i++){
for(int j=w[i];j<=m;j++)//这个正着循环即可
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
cout<<dp[m]<<endl;
return 0;
}
转多重背包
没错,完全背包还可以转多重背包。不过建议读者先阅读下面的多重背包再看这个算法。
其实很简单。我们把多重背包里的定义成
来求解就可以了。
状态转移方程:
最后是代码部分:
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],dp[maxm];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
for(int k=0;k<=j/w[i];k++)
dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
}
}
cout<<dp[n][m]<<endl;
return 0;
}
当然,由于多重背包的朴素算法的复杂度是的,所以必须优化(参见下面多重背包部分)。
多重背包
例题:P1776
朴素算法
多重背包 0-1背包 的差别就是 0-1背包 里一个物品只有一件,但多重背包里一个物品有s[i]件。
所以,我们分类讨论:
- s[i]*w[i]>=m 转完全背包
- 转0-1背包。拿一个 k 去枚举数量,于是问题变成了一个重量为
,价值为
的物品取不取
来人!上代码!
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],s[maxn],dp[maxm];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i]>>s[i];
for(int i=1;i<=n;i++){
if(s[i]*w[i]>=m){//相当于完全背包(因为比背包容量m大)
for(int j=w[i];j<=m;j++)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
else{//否则转0-1背包
for(int j=m;j>=w[i];j--){
for(int k=s[i];k>=0;k--){//枚举数量
if(j>=k*w[i])
dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);
}
}
}
}
cout<<dp[m]<<endl;
return 0;
}
显然,时间复杂度不是一般的高,是二般的高。
所以优化是必须的。接下来我们来看多重背包的两种优化。
多重背包的二进制优化
思路
我们想一下,我们当时用 k 从枚举到0是否必要。
我们其实可以合并一些物品。
所以我们可以只枚举一些物品,通过这些物品互相合并,产生新的物品。比如:10=1+2+4+3
那为什么不拆成1+2+7呢?因为这样你就无法合成出4了。
那怎么枚举呢?这里要用到一点倍增的思想了。
给大家看一下局部代码:
int a,b,s;//a是重量,b是价值,s是数量
cin>>a>>b>>s;
//如何合成这些多余的物体
int k=1;
while(k<=s){
w[++cnt]=k*a;//合成的重量
v[cnt]=k*b;//合成的价值
s-=k;//物品数相应减少
k*=2;//翻倍
}
if(s){//没办法翻倍了
w[++cnt]=s*a;
v[cnt]=s*b;
//剩下的自动合成
}
这样就OK了。
Code
//由于直接枚举取多少个会TLE
//所以我们进行二进制优化
//思考一下,其实我们没必要枚举0-s
//我们可以不断倍增:1,2,4,8,16,32,64,128...
//取这些数量个物品
//这样,通过不断组合可以组合出所有种类
//比如:10=1+2+4+3
//为什么有个3呢?
//因为正好剩个3了
//靠1,2,4,3可以组成0-10的所有数
//5=1+4 6=2+4 7=3+4 8=3+4+1 9=2+3+4 10=1+2+3+4
//所以在枚举时,我们不断倍增就可以了
//这样可以大大减少枚举量
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],dp[maxm];
int main(){
int n,m;
cin>>n>>m;
int cnt=0;//记录新物体数(相当于优化后的n)
for(int i=1;i<=n;i++){
int a,b,s;
//a是重量,b是价值,s是数量
cin>>a>>b>>s;
//接下来是重中之重
//如何合成这些多余的物体
int k=1;
while(k<=s){
w[++cnt]=k*a;//合成的重量
v[cnt]=k*b;//合成的价值
s-=k;//物品数相应减少
k*=2;//翻倍
}
if(s){//没办法翻倍了
w[++cnt]=s*a;
v[cnt]=s*b;
//剩下的自动合成
}
}
//0-1背包
for(int i=1;i<=cnt;i++){
for(int j=m;j>=w[i];j--)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
cout<<dp[m]<<endl;
return 0;
}
我个人认为这个方法好懂,好写,比下面的单调队列优化好一些。
单调队列优化多重背包
单调队列优化的思想就是给拿完后剩余体积分类,并且剩余体积一定不大于w,不然就能再拿。
我们可以枚举剩余体积,代码如下:
for(int i=1;i<=n;i++) //->物品个数
for(int j=0;j<w;j++) //->剩余体积
for(int k=j;k<=m;k+=w) //->不同个数
到这应该不难理解。
接下来,我们把这些状态表示一下:
(剩余体积 0)
(剩余体积 1)
(剩余体积 j)
于是,我们把问题分成 j 类,每类就是一个单调队列。
我们把v提出来,就是:
所以,我们用一个队列维护最大值的下标,每次入队即可。
但是由于我们要进行比较,所以我们还需要一个 pre 数组维护上一轮的队列就OK啦。
//单调队列优化的思想就是给拿完后剩余体积分类
//剩余体积一定<w,不然就可以再装
//0,1,2,3,......,w-1
//怎么枚举呢?看代码:
//for(int i=1;i<=n;i++) ->物品个数
// for(int j=0;j<w;j++) ->剩余体积
// for(int k=j;k<=m;k+=w) ->不同个数
//我们把这些状态表示一下:
//dp[0] dp[w] dp[2*w] ...... (剩余体积0)
//dp[1] dp[w+1] dp[2*w+1] ...... (剩余体积1)
//......
//dp[j] dp[w+j] dp[2*w+j] ...... (剩余体积j)
//我们把问题分为j类,每类就是一个单调队列
//dp[j]=dp[j]
//dp[j+w]=max{dp[j]+v,dp[j+w]}
//......
//dp[j+k*w]=max{dp[j]+k*v,dp[j+w]+(k-1)*v,......,dp[j+k*w]}
//我们稍微变换一下:
//dp[j]=dp[j]
//dp[j+w]=max{dp[j],dp[j+w]-v}+v
//dp[j+2*w]=max{dp[j],dp[j+w]-v,dp[j+2*w]-2*v}+2*v
//......
//dp[j+k*w]=max{dp[j],dp[j+w]-v,dp[j+2*w]-2*v,......,dp[j+k*w]-k*v}+k*v
//相当于把价值v挪到max函数外了
//所以说,我们每次入队dp[j+k*w]-k*w,维护最大值即可
//que维护的是dp[j+k*w]-k*w最大值的下标
//pre维护的是上一次的que
//每次比较一下即可
#include <bits/stdc++.h>
using namespace std;
const int maxm=40005;
int dp[maxm],pre[maxm],que[maxm];//que是队列
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
int w,v,s;
cin>>w>>v>>s;
memcpy(pre,dp,sizeof(dp));
for(int j=0;j<w;j++){//j个单调队列,j枚举的是拿完后剩下的重量(最多剩w-1)
int head=0,tail=-1;//头尾指针
for(int k=j;k<=m;k+=w){//k是取(k-j)/w件物品的重量
if(head<=tail && k-s*w>que[head])
head++;
while(head<=tail && pre[que[tail]]-(que[tail]-j)/w*v<=pre[k]-(k-j)/w*v)
tail--;
if(head<=tail)
dp[k]=max(dp[k],pre[que[head]]+(k-que[head])/w*v);
que[++tail]=k;
}
}
}
cout<<dp[m]<<endl;
return 0;
}
复习(混合背包)
Review
混合背包就是 0-1背包、完全背包、多重背包 的结合。
内容 | 状态转移方程 | 注意事项 | |
0-1背包 | 物品选/不选 | 倒着循环 | |
完全背包 | 物品无数件 | 正着循环 | |
多重背包 | 物品 | 优化 |
思路
混合背包还是输入,分别表示重量,价值,数量。然后进行分类讨论:
- 当
时,是 0-1背包。
- 当
时,是完全背包。
- 其他为多重背包。
然后套用以上的状态转移方程即可。
代码
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],s[maxn],dp[maxm];
int main(){
int n,m;
cin>>m>>m;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i]>>s[i];
for(int i=1;i<=n;i++){
if(s[i]==1){//0-1背包
for(int j=m;j>=w[i];j--)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
else if(s[i]==0){//完全背包
for(int j=w[i];j<=m;j++)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
else{//多重背包(朴素),转0-1背包
for(int j=m;j>=w[i];j--){
for(int k=1;k<s[i] && k*w[i]<=m;k++)
dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);
}
}
}
cout<<dp[m]<<endl;
return 0;
}
分组背包
例题:P1757
思路
和 0-1背包 类似,只用一个二维数组存储每个组的解就行了。每组按 0-1 背包的解法做就ok了。
代码
#include <bits/stdc++.h>
using namespace std;
vector<int> w[maxn],v[maxn];
int dp[maxm];
int main(){
for(int i=0;i<maxn;i++){
w[i].push_back(0);
v[i].push_back(0);
}
int m,n;
cin>>m>>n;
int kind=0;//种类数
for(int i=1;i<=n;i++){
int a,b,c;//a是重量,b是价值
cin>>a>>b>>c;//c是种类
w[c].push_back(a);
v[c].push_back(b);
kind=max(kind,c);
}
for(int i=1;i<=kind;i++){
for(int j=m;j>=0;j--){//倒着循环(0-1)背包
for(int k=1;k<(int)w[i].size();k++){
if(j>=w[i][k])
dp[j]=max(dp[j],dp[j-w[i][k]]+v[i][k]);
}
}
}
cout<<dp[m]<<endl;
return 0;
}
结尾
这篇 blog 用了我整整 1 天的时间整理,主要因为之前的背包的 blog 太散了,之后还会整理线性 dp 和区间 dp 的blog。看在我写了 7k+ 字的份上,点个赞再走吧。
友情提醒:虽然模板都是正确的,但直接提交会让你喜提 0 分(里面的 maxn 我都没有定义)