今天写了一天的背包问题(直接给我人写麻了),发个文章记录一下各种背包问题的经典例题的解法。
01背包
01背包可以说时后面问题的基础了,后面所有问题都要从01背包推导而来,只要掌握了01背包的思想,后面还是蛮好掌握的。那么接下来直接先看题。
动态规划中最关键的一个步骤就是设计状态,在这里我们定义dp[i][j]为只考虑前i个物品且最大容量为j时所能获得的最大利益,对于一个物品来说,我们只需要考虑拿还是不拿,不拿dp[i][j]价值为dp[i-1][j],拿的话dp[i][j]价值为dp[i-1][j-v[i]+w[i],所以在容量足够下状态转移方程为
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i])
,代码如下:
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(j>=v[i]){//容量足够时
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);//比较拿和不拿哪个价值更多
else{//容量不足时只能选择不拿
dp[i][j]=dp[i-1][j];
}
}
}
空间复杂度的优化
可以看到循环中我们只会用到dp[i][j]和dp[i-1][j],所以这里没必要使用二维数组,直接用一维数组对其进行覆盖就行了。一维数组dp[i]表示容量为i时的最大利益为dp[i]。所以这里的状态转移方程就是
dp[j]=max(dp[j],dp[j-v[i]]+w[i])
具体代码如下:
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
需要注意的是这里第二层循环必须从大到小进行,因为在二维数组中dp[i-1][j]和dp[i][j]是相互独立的,而在一维数组中dp[j]会由更小的状态dp[j-v[i]]+w[i]转移而来,如果dp[j-v[i]]先更新会出现同一个物品多次选择的情况,即dp[3]为选择一个i物品最优,进行到dp[5]时,可能dp[5]=dp[3]+w[i],这样就选择了两次i物品。
完全背包
完全背包与01背包的唯一差别就是物品可以重复选择,在01背包中我们只需要考虑是否选择这个物品,而在完全背包中我们需要考虑选择几次这个物品最优,所以状态转移方程就是
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i])
这里代码和01背包唯一差异就是后面的dp[i-1][j-v[i]]+w[i]变成了dp[i][j-v[i]]+w[i],因为这里要讨论无限多的物品拿多少最优,所以要从已更新的状态去更新这个状态(这个点我在01背包最后也说到了),所以要从dp[i]而不是dp[i-1]转移过来,具体代码如下
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(j>=v[i]){
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]);
}
else{
dp[i][j]=dp[i-1][j];
}
}
}
空间复杂度的优化
和01背包一样,这里的二维数组只用到两层,所以可以用一维数组不断覆盖来替代这里状态转移方程和01背包一样,也是
dp[j]=max(dp[j],dp[j-v[i]]+w[i])
但不同的是这里更新顺序不一样,第二层循环j要从小到大,即dp[j]由已更新的dp[j-v[i]+w[i]来更新(在01背包最后有这个的具体解释),最后附上代码
for(int i=1;i<=n;i++){
for(int j=v[i];i<=m;j++){
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
多重背包
时间复杂度O(nml)
在多重背包中,一个物品只能拿有限次,这里我们只需要多加一重循环对这个物品拿几次最优进行讨论即可,不多说,直接上代码
for(int i=1;i<=n;i++){//这里用的是01背包优化空间复杂度的写法,代码比较简洁
for(int k=1;k<=l[i];k++){
for(int j=m;j>=v[i];j--){
dp[j]=max(dp[j-v[i]]+w[i],dp[j]);
}
}
}
时间复杂度(nmlogl)
可以看到这题数据量较大,上面的写法很明显不能满足我们的需求,所以得优化一下我们的算法。
二进制优化
对于一个只能取s次物品,我们可以把它拆成s个数量为1的物品,这样这个问题就转换成了01背包问题,但是如果这样拆时间复杂度是不变的,所以我们需要采用最少的拆法,即按二进制拆。首先我们需要知道
s=2^0 + 2^1 + 2^2 + 2^3+……+k(k为余数)
这样拆分s可以选取其中部分和表示1-s中任意一个数,所以我们把可以选s次的物品拆分成logs个体积为v * 2^n,价值为w * 2^n的物品,这样时间复杂度就变成了O(nmlogs)级别,具体代码如下
for(int i=1;i<=n;i++){
cin>>v>>w>>s;
ll V[maxn],W[maxn],cnt=1;
//将s按二进制拆分
for(int i=1;i<=s;i<<=1){
V[cnt]=i*v;
W[cnt++]=i*w;
s-=i;
}
//s若有余数,单独储存
if(s){
V[cnt]=s*v;
W[cnt++]=s*w;
}
//直接用01背包处理即可
for(int k=1;k<=cnt;k++){
for(int j=m;j>=V[k];j--){
dp[j]=max(dp[j],dp[j-V[k]]+W[k]);
}
}
}
分组背包
分组背包这里还是可以直接看出来他和01背包有很多相似的,一组物品可以视之为01背包中的一个物品,对于一组物品而言只有取和不取两种状态,同时还需要再用一个循环讨论取这一组的哪一个达到该体积下的最优,代码如下
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>temp;
maxx=max(maxx,temp);//maxx找到最大组号便于后面的遍历
cnt[temp]++;//cnt[]记录第temp组的size
cin>>v[temp][cnt[temp]]>>w[temp][cnt[temp]];
//体积和价值要用二维数组储存
}
for(int i=1;i<=maxx;i++){//遍历组号
for(int j=m;j>=0;j--){//遍历体积
for(int k=1;k<=cnt[i];k++){//遍历该组内物品
if(j>=v[i][k]){//当可以取时看是否更优
dp[j]=max(dp[j],dp[j-v[i][k]]+w[i][k]);
}
}
}
}
二维背包
二维背包只是多了一个限制条件,原本只有体积的限制条件,现在受体积和体力的限制,所以处理起来也很简单,让dp数组多加一维且多加一个for对其进行讨论就行了,理解了01背包写这题还是很轻松的,最后附上代码
for(int i=1;i<n;i++){
cin>>v[i]>>w[i]>>t[i];
//v为体积,w为价值,t为体力
}
//这里类似于01背包的代码
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
for(int q=k;q>=t[i];q--){
dp[j][q]=max(dp[j][q],dp[j-v[i]][q-t[i]]+w[i]);
}
}
}
混合背包
混合背包就是单纯的把01背包,完全背包,多重背包混在了一起而已,所以思路也很简单,就是对应其类型,用其相应的解法就行了。这里01背包可以视之为多重背包的特殊情况,稍微缩短一点代码,但直接分三种情况写更直观,所以我没有采用那种写法。(顺便提一句,这题的数据量较少,多重背包不使用二进制优化也能过)
#include<bits/stdc++.h>
#define ll long long
#define rep(i,a,b) for(int i=a;i<b;i++)
#define endl '\n'
#define mem(a) memset(a,0,sizeof(a))
#define lowbit(x) x&-x
using namespace std;
const int mod=1e9+7;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int maxn=1e3+5;
ll dp[maxn],n,m,v,w,l,mode;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>mode>>v>>w;
if(mode==1){//01背包
for(int j=m;j>=v;j--){
dp[j]=max(dp[j],dp[j-v]+w);
}
}
if(mode==2){//完全背包
for(int j=v;j<=m;j++){
dp[j]=max(dp[j],dp[j-v]+w);
}
}
if(mode==3){//多重背包
cin>>l;
ll cnt=1,W[maxn],V[maxn];
for(int i=1;i<=l;i<<=1){
W[cnt]=i*w;
V[cnt++]=i*v;
l-=i;
}
if(l){
W[cnt]=l*w;
V[cnt++]=l*v;
}
for(int i=1;i<cnt;i++){
for(int j=m;j>=V[i];j--){
dp[j]=max(dp[j],dp[j-V[i]]+W[i]);
}
}
}
}
cout<<dp[m];
return 0;
}
/*
*/