初见 | 动态规划丨背包
背包
本文主要总结背包,都是很基础的背包,也是动态规划问题中的很重要的一类
这里不再去仔细推得状态转移方程
01背包
顾名思义,01背包的每种物品只有两种情况:选和不选
一般的01背包的问题都是这样描述的:
有n件物品,每个物品都有自己的费用v(占的体积或重量),价值c,所有的物品都只有一件
你有一个背包,这个背包的最大容量(最大的可占体积或最大的能承载重量)是m
问:在不超出m的情况下,怎么能使得背包内物品的价值最大?
简要分析
由01背包的特殊性,我们可以知道每种物品都只有选和不选两种情况,每个物品在选的时候都要考虑剩余的背包容积和当前价值是否最优,那么我们使用f[x]来表示在体积为x的时候的最大价值
再加以图表分析,我们就可以很简单的得出状态转移方程:
f[x]=max(f[x],f[x-v[i]+m[i]]);
这里因为我们在选当前物品的时候要取决于上个物品的选择和上个物品在不同体积下的最大价值,所以我们考虑逆推,那么我们得到的01背包的核心程序就如下:
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+c[i]);
//应该注意的是max函数需要你自己声明或使用<algorithm>库
由此我们得到了基础但也非常重要的一个状态转移方程,有了这个方程,下面我们只需要类比就能简单的推出完全背包和多重背包的状态转移方程
完全背包
完全背包在描述上只有一点和01背包不一样,即为:
每个物品能取无限次
考虑到它和01背包的相关性,我将类比01背包来推方程
类比分析
考虑到能取无限个,那完全背包实际上就是无数个01背包,唯一不同的是这里不能再去考虑倒序得解决,原因也很简单:每个物品可以取无限次
实际上最后推得的状态转移方程和01背包是一致的,这是因为他们都是要考虑当前物品能否取得最优解和当前所剩空间大小
f[x]=max(f[x],f[x-v[i]+m[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]]+c[i]);
//还是应该注意的是max函数需要你自己声明或使用<algorithm>库
其实只有第二层循环的差别,一个正序,一个倒序
实际上到这里我们最基础的两个背包就完成了,后面的(我认为)都可以由这两个直接或间接的推得
多重背包
多重背包也是和01背包只有一点不同:它每个物品能取有限个
如何求解?
可能有些质朴中和,志虑忠纯的同学就直接说了:
在01背包的基础上,再加一层循环不就完事了吗
有一说一,确实如此,这也是多重背包最好想的一种方案,那么我们只需要再来一层循环即可
但是相应的,很明显这个三层循环的复杂度瞬间就上去了,使用这样朴素的程序坑能让我们在面对稍微大一点的数据的时候就面临TLE(Time Limit Enough)而与AC(All Cofusing answers)失之交臂
那么我们就需要优化,那怎么优化呢?
说实话不看书我也不知道
这里引入一个全新的优化方案
注:这里指对我全新,大佬肯定都见过
二进制优化
这是一个十分高大上的名字,短短五个字就能让我感到无比的悲哀
但是我现在不再悲哀,因为我至少会了点,下面我来说说多重背包的二进制优化方向
作为一个和01背包相似的背包,我们的优化方式就是通过二进制将多重背包优化为01背包,从而削掉一层循环!
具体的思路就是将当前物品能选的次数拆开,拆成2n个只能选一次的物品
为了好理解,我来举个例子:
比如说当前物品能取27次,那它可以拆成:20+21+22+23+(27-20+21+22+23)=1+2+4+8+12
27和这些数字化为二进制就是:11011,1,10,100,1000,1100
这里显然后面的五个数可以拼出1~27的所有数
那这里我们就把27拆为了五个价值和费用都为其1倍的,2倍的,4倍的,8倍的,12倍的物品,且这五个物品都只能选一次
假如我们把题目中的每一件物品都这样拆分的话,就将多重背包变为了一个01背包,直接削掉一层循环,向着MLE,CE,UKE,WA,等
向着AC又迈出了一步!
那么代码怎么打呢?
这就非常简单了,相信聪明的大佬们总能读懂我的代码吧!
#include <iostream>
#include <algorithm>
#include <stdio.h>
#include <cstring>
#define HRiver2 return
#define Warma 0
#define ll long long
using namespace std;
int f[100015],n,m,v[100015],c[100015],n1;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int t=1,x,y,z;
cin>>y>>x>>z;
while(z>=t)
{
v[++n1]=x*t;
c[n1]=y*t;
z-=t;
t*=2;
}
v[++n1]=x*z;
c[n1]=y*z;//上面这块在实现拆分
}
for(int i=1;i<=n1;i++)
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+c[i]);//这里就同01背包了
cout<<f[m];
HRiver2 Warma;
}
没错,学完这个就能直接切一道水绿题:洛谷P1776宝物筛选
混合背包
化学L伟老师曾云:把简单的事情做好就是不简单
众所周知,化学,是一个中心学科,与其他学科联系密切(源自必修一),所以信息这里也能用上化学老师说的话
只不过需要扩展一下:简单的事情组合起来就不简单
上面的都是瞎扯,因为混合背包还是简单的事情
混合背包,顾名思义,就是01,多重,完全三者结合,有些物品只能取1次,有些能取无限次,有些能取有限次
那我们怎么办呢?
那肯定是分类讨论啊,这不基本思想吗?
因为和上面差不多,也没什么好讲的,就直接上代码吧
代码实现
#include <iostream>
#include <algorithm>
#include <stdio.h>
#define HRiver2 return
#define Warma 0
#define ll long long
using namespace std;
ll t1[3],t2[3],n,v[100015],c[100015],p[100015],m,n1,f[10000015];
int main()
{
scanf("%lld:%lld %lld:%lld",&t1[1],&t1[2],&t2[1],&t2[2]);
t1[3]=t1[1]*60+t1[2];t2[3]=t2[1]*60+t2[2];
m=t2[3]-t1[3];
cin>>n;
for(int i=1;i<=n;i++)
{
ll x,y,z;
scanf("%lld %lld %lld",&x,&y,&z);
if(z==0)
{
v[++n1]=x;
c[n1]=y;
p[n1]=0;
}
else if(z==1)
{
v[++n1]=x;
c[n1]=y;
p[n1]=1;
}
else
{
int t=1;
while(z>=t)
{
v[++n1]=x*t;
c[n1]=y*t;
p[n1]=1;
z-=t;
t*=2;
}
v[++n1]=x*z;
c[n1]=y*z;
p[n1]=1;
}
}
for(int i=1;i<=n1;i++)
{
if(p[i]==1)
{
for(int j=m;j>=v[i];j--)
{
f[j]=max(f[j],f[j-v[i]]+c[i]);
}
}
else if(p[i]==0)
{
for(int j=v[i];j<=m;j++)
{
f[j]=max(f[j],f[j-v[i]]+c[i]);
}
}
}
cout<<f[m];
HRiver2 Warma;
}
学完这个又可以轻轻松松切一道水绿题:洛谷P1833
分组背包+二维费用背包
之所以把这两个放在一起,是因为都是01背包的Higher Level
前者只需要加上一层组别循环,后者只需多加一维费用,看代码可能会更好一点吧
分组背包代码实现
#include <iostream>
#include <stdio.h>
#include <algorithm>
#define HRiver2 return
#define Warma 0
using namespace std;
int f[1015],n,m,v[1015],c[1015],a[1015][1015],p,x,t;
int main()
{
cin>>m>>n;
for(int i=1;i<=n;i++)
{
cin>>v[i]>>c[i]>>p;
a[p][++x]=i;
t=max(t,p);
}//分组背包这里要将背包分到它的组别里面,并找出最大组别号
for(int i=1;i<=t;i++)
{
for(int j=m;j>=0;j--)
{
for(int u=1;u<=x;u++)
{
int y=a[i][u];
if(j>=v[y])
{
f[j]=max(f[j],f[j-v[y]]+c[y]);
}
}
}
}
cout<<f[m];
HRiver2 Warma;
}
二维费用背包代码实现
(这里我只写关键步骤,毕竟作为课本例题,课本上面都有)
for(int i=1;i<=n;i++)
for(int q=v;q>=0;q--)
for(int r=u;r>=0;r--)
{
int t1=v+a[i],t2=u+b[i];
if(t1>v)t1=v;
if(t2>u)t2=u;
f[t1][t2]=max(f[t1][t2],f[q][r]+c[i]);
}
背包问题的方案数
这实际上是一个比较笼统的分类,因为上述每一种都可能出现分类问题
而解决这些问题的方案很简单,把max改为sum即可,因为f[x]若作为容量为x时的方案数,刚才的方程实际上已经考虑到了所有情况
举个例子:完全背包的方案数就是:
for(int i=1;i<=n;i++)
for(int j=a[i];j<=m;j++)
f[j]+=f[j-v[i]];
End
最后总结的话,这些背包的大概关系是这样的
01背包 | 完全背包 | 多重背包 | 混合背包 | 分组背包 | 二维费用背包 | 背包方案数 |
---|---|---|---|---|---|---|
01背包 | 01背包+ | 01背包Pro | 01背包Pro+ | 01背包S | 01背包Ultra | 01背包Premium Edition |
(和某色米K30/40系列市场部直呼内行)