在我的上一篇博客中,我已简单梳理了DP类型的解题思路并列举了一道经典线性DP(数字三角形),这篇博客我将梳理背包类型DP的思路及模板。
(因为代码是我不同时期写的所以码风会略有不同)
01背包
采药,01背包板子题
- 略
- 以每件物品划分状态
- 设置 f [ i ] [ j ] f[i][j] f[i][j]为取到第i件物品,使用容量j时的最优解
- 很显然,每件物品有选和不选两种状态,于是可以得到 f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − w [ i ] ] + v [ i ] f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i] f[i][j]=max(f[i−1][j],f[i−1][j−w[i]]+v[i]
代码:
#include <bits/stdc++.h>
using namespace std;
int f[105][1005];
int main()
{
int t,m;scanf("%d%d",&t,&m);
int w,v;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&w,&v);
for(int j=0;j<=t;j++)//遍历重量
if(j>=w)f[i][j]=max(f[i-1][j],f[i-1][j-w]+v);//能装下
else f[i][j]=f[i-1][j];//装不下
}
printf("%d",f[m][t]);
return 0;
}
通过观察不难发现, f [ i ] [ j ] f[i][j] f[i][j]的值实际上指和j有关系,所以就可以进行降维,省去第一维。
需要注意的是,降维后重量的遍历需要从大向小,原因:
假设从小向大遍历,若当前重量 j j j能够被选择,那么之后的重量 k ∗ j ( k ∗ j ≤ t ) k*j(k*j≤t) k∗j(k∗j≤t)也同样能被选择,进行状态转移 f [ k ∗ j ] = m a x ( f [ k ∗ j ] , f [ ( k − 1 ) ∗ j ] + v ) f[k*j]=max(f[k*j],f[(k-1)*j]+v) f[k∗j]=max(f[k∗j],f[(k−1)∗j]+v)时,因为 f [ ( k − 1 ) ∗ j ] f[(k-1)*j] f[(k−1)∗j]已经被遍历,所以实际上选择了两件相同物品。
代码:
#include <bits/stdc++.h>
using namespace std;
int f[1005];
int main()
{
int t,m;scanf("%d%d",&t,&m);
int w,v;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&w,&v);
for(int j=t;j>=w;j--)
f[j]=max(f[j-w]+v, f[j]);
}
printf("%d",f[t]);
return 0;
}
完全背包
疯狂的采药完全背包板子
上面的01背包降维正序遍历很显然就是完全背包的递推式,因为它可以重复选取并且毫无限制。
所以完全背包只要把01的降维代码重量遍历方向转换一下就可以了,代码不给了。
多重背包
庆功会多重背包板子题
每件物品可以选 s s s件,换种方式理解,有 s s s件费用和价值一样的物品,只需要每件物品做 s s s次01背包就可以了。
代码:
#include <bits/stdc++.h>
using namespace std;
int f[6005];
int main()
{
int t,m;scanf("%d%d",&m,&t);
int w,v,s;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&w,&v,&s);
while(s--)
for(int j=t;j>=w;j--)
f[j]=max(f[j-w]+v, f[j]);
}
printf("%d",f[t]);
return 0;
}
这个代码虽然好写,但是时间复杂度太高,当物品数量过多,就会彻底爆炸。
因为 s s s件物品的所有属性都一样,所以可以利用倍增的思想进行二进制优化。
众所周知,二进制可以表示出十进制中的任意数,所以可以将 s s s件物品近似地拆解成 log 2 s \log_2^s log2s件物品,再对其进行01背包选择,时间复杂度一下子就降了好多!
代码:
#include<bits/stdc++.h>
#define max(a,b) (a>b?a:b)
using namespace std;
inline int in()
{
int x=0;char c=getchar();
while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9'){x=x*10+c-48;c=getchar();}
return x;
}
int f[6005];
int v[35],w[35];
int main()
{
int n=in(),m=in();
int vi,wi,si;
while(n--)
{
vi=in(),wi=in(),si=in();
int k=1,cnt=0;
while(k<=si)v[++cnt]=vi*k,w[cnt]=wi*k,si-=k,k<<=1;//二进制拆解,并将其记录
if(si)v[++cnt]=vi*si,w[cnt]=wi*si;//拆剩下的
for(int i=1;i<=cnt;i++)
for(int j=m;j;j--)
if(j>=v[i])f[j]=max(f[j],(f[j-v[i]]+w[i]));//再进行01背包选择
}
printf("%d",f[m]);
return 0;
}
混合背包
当你熟练掌握前三种背包时,这类题目就是纯纯的水题,这里提供两种解法
解法一
每个物品进行一个判断,01和多重倒序遍历重量,完全正序
代码:(二进制优化就没加了)
#include<bits/stdc++.h>
using namespace std;
int f[205],w[35], c[35], p[35];
int main()
{
int m,n;scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)scanf("%d%d%d",&w[i],&c[i],&p[i]);
for(int i=1;i<=n;i++)
{
if(!p[i])//完全背包
for(int j = w[i];j<=m;j++)
f[j]=max(f[j],f[j-w[i]]+c[i]);
else//多重背包(01背包可当成物品数量为1的完全背包)
for(int j=m; j>=w[i];j--)
for(int k=0; k*w[i]<=j&&k<=p[i];k++)
f[j]=max(f[j],f[j-k*w[i]]+k*c[i]);
}
printf("%d",f[m]);
return 0;
}
解法二
我写了那么长,调了那么久的二进制优化就用这么几遍真是太可惜了!(虽然上方代码没用)
所以可以将完全背包也转化为多重背包,物品数量为 ⌊ n / w ⌋ \lfloor {n/w}\rfloor ⌊n/w⌋
代码:
#include<bits/stdc++.h>
using namespace std;
const int M=1919810;
inline int in()
{
int x=0;char c=getchar();
while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9'){
x=x*10+c-48;c=getchar();
}
return x;
}
struct Node
{
int w,c,p;
}a[35];
int f[205];
int main()
{
int m=in(),n=in();
for(int i=1;i<=n;i++)
a[i].w=in(),a[i].c=in(),a[i].p=in(),a[i].p=!a[i].p?M:a[i].p;
for(int i=1;i<=n;i++)
{
for(int k=m;k;k--)
{
for(int j=0;j<=min(k/a[i].w,a[i].p);j++)
{
f[k]=max(f[k-j*a[i].w]+j*a[i].c,f[k]);
}
}
}
printf("%d ",f[m]);
return 0;
}
分组背包
每组只能选择一件物品,所以整体上将每组看做一个物品,进行01背包,每组内部再进行一次01
代码:
#include<bits/stdc++.h>
#define max(a,b) ((a)>(b)?(a):(b))
using namespace std;
inline int in()
{
int x=0;char c=getchar();
while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9'){x=x*10+c-48;c=getchar();}
return x;
}
int dp[205], w[35], c[35];
vector<int> g[15];//物品
int main()
{
int v=in(),n=in(),t=in(),p;
for(int i = 1; i <= n; ++i)
{
w[i]=in(),c[i]=in();
g[in()].push_back(i);
}
for(int i=1;i<=t;i++)
{
for(int j=v;j>=0;j--)
{
for(int k=0;k<g[i].size();k++)
{
int x=g[i][k];
if(w[x]<=j)dp[j]=max(dp[j],dp[j-w[x]]+c[x]);
}//每组内部
}
}
printf("%d",dp[v]);
return 0;
}
这里有个STL的巧妙运用,使用vector存储每组内部物品,可以直接通过访问size()函数得到当前组的物品个数,就不用单独存储了。
二维费用
因为两种费用,所以 f f f数组也对应的扩展到二维,以此类推, p p p种费用就需要开 p p p维的数组(不过三维以上的题目就比较罕见了)
代码:
#include<cstdio>
#define max(a,b) (a>b?a:b)
#define min(a,b) (a>b?b:a)
using namespace std;
inline int in()
{
int x=0;char c=getchar();
while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9'){x=x*10+c-48;c=getchar();}
return x;
}
int f[1005][505];
int main()
{
int n=in(),m=in(),k=in(),w1,w2;//1为球数,2为伤害
for(int i=0;i<k;i++)
{
w1=in(),w2=in();
for(int j=n;j>=w1;j--)
for(int k=m;k>=w2;k--)
f[j][k]=max(f[j][k],f[j-w1][k-w2]+1);
}
printf("%d ",f[n][m]);
int mn=m;
for(int i=1;i<=m;i++)
if(f[n][i]==f[n][m]){mn=i;break;}//在收服最多精灵的基础上体力损耗最少
printf("%d",m-mn);
return 0;
}
(这是目前为止我写得最长的一篇博客了所以请点个免费的赞吧秋梨膏)