背包问题

01背包

题目描述
有一个体积为M的背包,N种物品(每种物品只有一个),每种物品都有对应的体积v[i],价值w[i],将哪几件物品放入背包可使得背包中的物品价值之和最大且不超过背包的体积。

思路
每种物品只有一个,对于每种物品只有放入背包与不放入背包两种选择。
令f[i][j]表示前i个物品放入体积为j的背包的最大价值,最终答案为f[M][N]。
状态转移方程:\(f[i][j]=max(f[i-1][j-v[i]]+w[i],f[i-1][j])\),可以用数学归纳法证明其正确性。

核心代码

for(int i=1;i<=N;++i){//遍历物品
  for(int j=0;j<=M;++j){//遍历容量
    if(j>=v[i])f[i][j]=max(f[i-1][j-v[i]]+w[i],f[i-1][j]);
    else f[i][j]=f[i-1][j];
  }
}

空间优化
通过状态转移方程可以看出,f[i]这一层的状态只与f[i-1]这一层有关,那么当i很大时,会保存很多的冗余数据,故可考虑空间优化。
最直观的方法为开两个一维数组,每次只保存相邻的两个状态。
另一种方法为开一个一维数组,更新数组时倒序遍历数组。(注意到f[i][j]只与之前(左边)的状态(f[i][j-v[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]);

完全背包

题目描述
完全背包与01背包的不同之处在于完全背包的每种物品都有无限多个。

思路
由于每种物品有无限个,故每种物品的选择有多种,放入0个到背包,放入1个到背包,放入2个到背包...一直达到背包的体积上限。
令f[i][j]表示前i个物品放入体积为j的背包的最大价值,最终答案为f[M][N]。
状态转移方程:\(f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i],f[i-1][j-2*v[i]]+2*w[i],...f[i-1][j-n*v[i]]+n*w[i])\),\(\quad\) \(n*v[i]\le j\)

优化
将状态转移方程改写:
\(f[i][j]=max(f[i-1][j],\)
\(max(f[i-1][j-v[i]]+w[i],f[i-1][j-2*v[i]]+2*w[i],...f[i-1][j-n*v[i]]+n*w[i]))\),\(\quad\) \(n*v[i]\le j\)
在求最大值时若能将后面那一部分看成整体,这个整体表示的意思时当前物品放1个及以上的最大价值,则转移方程又与01背包有了相同的形式。
先看代码

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]);

可以看到表示容量的那一层循环的遍历方向发生了改变。
假设先在有体积为3的物品J,价值为w,从f[3]=max(f[3-3]+w,f[3])可知
f[3]表示当背包体积为3时,放入0个J与放入1个J的最大价值。
f[6]=max(f[6-3]+w,f[6])=max(f[3]+w,f[6]),由于f[3]表示放0个与1个的最大价值,则f[3]+w表示放1个与2个的最大价值,
括号里的f[6]表示一个都不放,则等式左边的f[6]表示放0个,1个,2个的最大价值。
f[9]=max(f[9-3]+w,f[9])...
经过分析可得代码中的f[j-v[i]]+w[i]表示当前物品放1个及以上的最大价值。
不是整数倍的体积状态分析过程类似。

事实上,对于完全背包,我们就是一直选择性价比高的物品,最后用性价比稍低的物品填补“空隙”。对于收益函数非线性的物品,这种方法将变得不在可取 ,因为其性价比会随着体积变化。

多重背包

问题描述
有一个体积为M的背包,N种物品,每种物品都有对应的体积v[i],价值w[i]以及个数n[i],将哪几件物品放入背包可使得背包中的物品价值之和最大且不超过背包的体积。

法一 暴力求解\(O(M\sum n[i])\)
转化为01背包求解,即将第i种物品转化为01背包中的n[i]个物品。

法二 二进制优化\(O(M\sum log\ n[i])\)
假如现在有一种商品,其个数为11,按照法一的做法,我们要枚举12次,(放0,1,2,...,12个物品到背包)。
若我们将11个物品打包成4分,每份物品的个数为1,2,4,4,显然1,2,4,4,可以表示0~12中的所有数字,此时只需要枚举4次。
这就是二进制优化,下面介绍下如何打包。


11=0111(B)+0100(B)
将11拆成两部分,第一部分是不超过11的最大的\(2^n-1\),第二部分是剩下的数(如果有的话)。
\(2^n-1\)的每一位都打包成一份物品(二进制状态下),
例如0111(B)变成0100(B),0010(B),0001(B)。
由二进制的性质可得,这些数能表示0~\(2^n-1\)的所有数。
由于第二部分数比小于等于第一部分数(不然你就拆错了),将第二部分打包,此时所有包裹的组合能表示所有物品数。

多重背包问题 II
代码

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=2e3+5;
int f[N];
struct good{
    int v,w;
};
vector<good>a;
int n,v,vi,wi,si,V;
int main(){
    cin>>n>>v;
    for(int i=0;i<n;++i){
        cin>>vi>>wi>>si;
        int s=si,k=1;
        for(k=1;k<s;k*=2){//读一个拆一个,拆完直接DP
            s-=k;//以2的k次方为单位来拆
            for(int j=v;j>=k*vi;--j)f[j]=max(f[j-k*vi]+k*wi,f[j]);
             //从这里可以看出第二部分数小于等于第一部分
        }
        //如果有剩下的
        if(s)for(int j=v;j>=s*vi;--j)f[j]=max(f[j-s*vi]+s*wi,f[j]);
    }
    cout<<f[v];
    return 0;
}

总结一下,二进制优化的东东一般都是将十进制数拆成2的整数幂

法三 单调队列优化\(O(NM)\)
大家可以去看下大佬的题解,写的很详细。
假设背包的体积为20,现在遍历到的物品体积为3,一共有4个,价值为w(因为无关紧要),我们来看下每个状态的求解是否有一定的规律。
f[i][0]=f[i-1][0]
f[i][1]=f[i-1][1]
f[i][2]=f[i-1][2]
以上状态都装不下体积为3的物品
f[i][3]=max(f[i-1][3],f[i-1][0]+w)
f[i][4]=max(f[i-1][4],f[i-1][1]+w)
f[i][5]=max(f[i-1][5],f[i-1][2]+w)
f[i][6]=max(f[i-1][6],f[i-1][3]+w,f[i-1][0]+2w)
f[i][7]=max(f[i-1][7],f[i-1][4]+w,f[i-1][1]+2w)
f[i][8]=max(f[i-1][8],f[i-1][5]+w,f[i-1][2]+2w)
f[i][9]=max(f[i-1][9],f[i-1][6]+w,f[i-1][3]+2w,f[i-1][0]+3w)
...
观察红色的状态,是不是发现了什么😯
f[i][0]=f[i-1][0]
f[i][3]=max(f[i-1][3],f[i-1][0]+w)
f[i][6]=max(f[i-1][6],f[i-1][3]+w,f[i-1][0]+2w)
f[i][9]=max(f[i-1][9],f[i-1][6]+w,f[i-1][3]+2w,f[i-1][0]+3w)
f[i][12]=max(f[i-1][12],f[i-1][9]+w,f[i-1][6]+2w,f[i-1][3]+3w,f[i-1][0]+4w)
f[i][15]=max(f[i-1][15],f[i-1][12]+w,f[i-1][9]+2w,f[i-1][6]+3w,f[i-1][3]+4w)没有5w因为这个东西最多只有4个
...
以上状态的求解用到了很多相同的状态,因为他们都是3的倍数,换句话说,他们模3有相同的余数。
因为当前物品最多有4个,所以max()里面的式子最多只有5个。这个5就是单调队列的长度。
这就启发我们求解状态时不要一个一个挨着求(f[i][0],f[i][1],f[i][2]...)
而是跳跃着求(f[i][r],f[i][r+v],f[i][r+2v]...)
这里的r=背包体积%当前物品体积,即一次性求出具有相同余数的体积状态。
多重背包问题 III
代码

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e3+5;
const int V=2e4+5;
int f[2][V];//这里要开两个数组,保存相邻的两层状态
int q[V],head,tail;
int main(){
    int n,v,vi,wi,si;
    cin>>n>>v;
    for(int i=1;i<=n;++i){
        cin>>vi>>wi>>si;
       
        for(int j=0;j<vi;j++){ 
        head=tail=0;
        for(int k=j;k<=v;k+=vi){//求出具有相同余数的体积状态
            while(tail!=head&&f[(i+1)&1][q[tail-1]]+(k-q[tail-1])/vi*wi<f[(i+1)&1][k])--tail;
             //上面的式子用来维护单调队列,队首为最优解
            while(head!=tail&&(k-q[head])/vi>si)++head;//队首的解可能不在范围内,去掉这个限制即可用来求完全背包
            q[tail++]=k;
            f[i&1][k]=f[(i+1)&1][q[head]]+(k-q[head])/vi*wi;//滚动数组
        }
    }
    }
    cout<<f[(n)&1][v];
    return 0;
}

混合背包

问题描述
有 N 种物品和一个容量是 V 的背包。
物品一共有三类:

  • 第一类物品只能用1次(01背包)
  • 第二类物品可以用无限次(完全背包)
  • 第三类物品最多只能用 si 次(多重背包)

每种体积是 v[i],价值是 w[i]。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。

思路
这种问题并没有什么新鲜的地方,只是将前三种问题组合起来。
混合背包问题
代码

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e3+5;
const int V=1e3+5;
int n,v,vi,wi,si;
int f[V];
int main()
{
    cin>>n>>v;
    for(int i=1;i<=n;++i){
        cin>>vi>>wi>>si;
        if(si==-1){//01背包
            for(int j=v;j>=vi;--j)
            f[j]=max(f[j],f[j-vi]+wi);
        }
        else if(si==0){//完全背包
            for(int j=vi;j<=v;++j)
            f[j]=max(f[j],f[j-vi]+wi);
        }
        else {//多重背包的二进制优化
            for(int j=1;j<=si;++j){
                si-=j;
                for(int k=v;k>=j*vi;--k)
                f[k]=max(f[k],f[k-j*vi]+j*wi);
            }
            if(si)
            for(int k=v;k>=si*vi;--k)
            f[k]=max(f[k],f[k-si*vi]+si*wi);
        }
    }
    cout<<f[v];
    return 0;
}

二维费用的背包

问题描述
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。
每件物品只能用一次。体积是 v[i],重量是 m[i],价值是 w[i]。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。

思路
和01背包的区别是增加了一个重量限制,我们增加一个维度表示这种限制就好。
f[i][j][k]表示前i个物品放入体积为j重量为k的背包(i这个维度可省略)
二维费用的背包问题
代码

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e3+5;
const int V=105;
const int M=105;
int f[V][M];
int n,v,m,vi,mi,wi;
int main(){
    cin>>n>>v>>m;
    for(int i=1;i<=n;++i){
        cin>>vi>>mi>>wi;
        for(int j=v;j>=vi;--j)//这里要遍历两种条件(维度)
        for(int k=m;k>=mi;--k)
        f[j][k]=max(f[j][k],f[j-vi][k-mi]+wi);
    }    
    cout<<f[v][m];
    return 0;
}

分组背包

问题描述
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 \(v_{ij}\),价值是 \(w_{ij}\),其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

思路
由于每组物品只能选一个,相当于又加了个限制。
我们回顾下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]]+w[i]);

循环的顺序为先循环物品,在循环容量,我们反转下循环的顺序。

for(int j=M;j>=v[i];--j)//遍历容量
     for(int i=1;i<=N;++i)//遍历物品
    f[j]=max(f[j],f[j-v[i]]+w[i]);

我们看下代码的含义,对于每个容量j,遍历所有物品,由于容量是倒叙遍历,
那么对于f[j],有f[j-v[i]]==0,化简即f[j]=max(f[j],w[i])
不难发现f[j]表示每件物品最多只选一件的最大价值
再回到分组背包问题,同一组内的物品最多只能选一个,套用相同的循环结构即可。
分组背包问题

//f[i][j]表示前i组,背包体积为j,由于只用到了相邻的两组状态
//所以减少一维
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int V=105;
int n,v,si,vi,wi;
int f[V];
int a[105];
int b[105];
int main(){
    cin>>n>>v;
    for(int i=1;i<=n;++i){//遍历每一组
        cin>>si;
        for(int j=0;j<si;++j)cin>>a[j]>>b[j];//读数据
        for(int j=v;j;--j)//倒序遍历体积,保证f[j-a[k]]中不含任意一个当前组内的物品
        for(int k=0;k<si;++k)//这个正序倒序都无所谓啦
        if(j>=a[k])f[j]=max(f[j],f[j-a[k]]+b[k]);
    }
    cout<<f[v];
    return 0;
}

有依赖的背包

问题描述
有 N 个物品和一个容量是 V 的背包。
物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。
如下图所示:

如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。
每件物品的编号是 i,体积是 v[i],价值是 w[i],依赖的父节点编号是 p[i]。物品的下标范围是 1…N。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
数据保证所有物品构成一棵树。

基本思路
树形DP
f[i][j]表示选择以i为根节点的子树,当前背包体积为j的最大价值。
详细见代码:
有依赖的背包问题

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=105;
const int V=105;
vector<int>g[N];
int f[N][V];//f[i][j]表示选择以i为根节点的子树,当前背包体积为j的最大价值。
int n,vv,v[N],w[N],pi,root;
void dfs(int s){
    for(int i=v[s];i<=vv;++i)f[s][i]=w[s];//父节点必选(题目要求)
    for(int i=0;i<g[s].size();++i){//遍历孩子节点,注意循环顺序是先循环物品,再循环容量
        int next=g[s][i];
        dfs(next);//dfs孩子节点,获得后面转移方程需要的数据
        for(int j=vv;j>=v[s];--j)//倒序遍历容量。因为每个物品只有一个
        for(int k=0;k<=j-v[s];++k)//分给子树的容量,注意要留够空间放父节点
        f[s][j]=max(f[s][j],f[s][j-k]+f[next][k]);//转移,尝试加入当前子树
    }
}
int main(){
    cin>>n>>vv;
    for(int i=1;i<=n;++i){
        cin>>v[i]>>w[i]>>pi;
        if(pi==-1)root=i;//记录根节点
        else g[pi].push_back(i);//建树
    }
    dfs(root);
    cout<<f[root][vv];//输出答案
}

背包问题求方案数

问题描述
条件与01背包相同,但要输出最佳选法的方案数。

思路
开个cnt[j]记录容量为j时的最佳选法的方案数即可。
背包问题求方案数
代码

#include<iostream>
#include<algorithm>
using namespace std;
const int mod=1e9+7;
const int N=1e3+5;
const int V=1e3+5;
int cnt[V];
int f[V],n,v,vi,wi;
int main(){
    cin>>n>>v;
    for(int i=0;i<n;++i){
        cin>>vi>>wi;
        for(int j=v;j>=vi;--j){
            if(f[j-vi]+wi>f[j])cnt[j]=max(1,cnt[j-vi]),f[j]=f[j-vi]+wi;//更新方案数,注意此时若cnt[j-vi]大于1就继承他的值
            else if(f[j-vi]+wi==f[j])cnt[j]=(cnt[j]+max(cnt[j-vi],1))%mod;//继承方案数
        }
    }
    cout<<cnt[v];
}

背包问题求具体方案

问题描述
条件与01背包一样,但要输出具体方案。

思路
这个时候数组的维数不能省略了,因为要用到中间的状态。
我们只用知道f[i]与f[i+1]是经过那个物品转移的即可。
背包问题求具体方案
代码

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e3+5;
const int V=1e3+5;
int f[N][V];//f[i][V]表示从i到最后一个,因为这个题目有输出顺序的要求
int n,v,vv[N],w[N];
int main(){
    cin>>n>>v;
    for(int i=1;i<=n;++i)cin>>vv[i]>>w[i];
    for(int i=n;i;i--){//反过来
        for(int j=0;j<=v;++j){
        f[i][j]=f[i+1][j];
        if(j>=vv[i])
        f[i][j]=max(f[i+1][j-vv[i]]+w[i],f[i+1][j]);//从这里可以看到选了这个物品则前后两个状态相差w[i]
    }
    }
    int m=v;
    for(int i=1;i<=n;++i){//倒过来找转移的路线
        if(v>=vv[i]&&f[i][v]==f[i+1][v-vv[i]]+w[i])cout<<i<<" ",v-=vv[i];
        //满足这个条件即说明两个状态是通过这个物品转移的,即这个物品选了
    }
    return 0;
}
posted @ 2022-02-05 17:15  何太狼  阅读(55)  评论(0编辑  收藏  举报