背包问题合集

一、01背包问题

1.1 01背包求最大价值

题目描述

题目链接:2. 01背包问题 - AcWing题库

思路

对于01背包,我们需要考虑2个方面,我们需要考虑选择哪些物品使价值最大,同时也要考虑体积问题。我们可以用一个二维数组来维护。对于f[i][j] ,i维持物品的种类,j维持空间的大小,值存的就是当前最大的价值。

对于集合f,我们要考虑条件,也就是题目上的限制,对于这个模板题,我们要从前i个物品中选,总体积小于j的。

对于属性,一般有max、min、数量……(即f存的值存要求的属性),这道题里找的最大值

状态计算本质就是集合的划分

代码

#include<iostream>
 
using namespace std;
 
const int N=1e4+10;
 
int n,m;
int v[N],w[N];
int f[N][N];
 
int main()
{
    scanf("%d %d",&n,&m);//n种物品,背包总体积为m
    for(int i=1;i<=n;i++)//输入n种物品,和每种的价值
        scanf("%d %d",&v[i],&w[i]);
    for(int i=1;i<=n;i++)
        for(int j=0;j<=m;j++)
        {
            f[i][j]=f[i-1][j];//不选物品i
            if(j>=v[i])//当体积大于v[i]时才有能装i的机会
                f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);//选物品i
        }
    printf("%d",f[n][m]);
    return 0;
}

一维优化

我们先举个例子

由于我们计算f[i]层时,只用到的f[i-1]层的数据

所以对于

f[i][j]=f[i-1][j];

我们可以删除第i维,就变成

f[j]=f[j]

所以这一行可以直接删去.

对于j,我们从0~m遍历,此时循环里就一个if循环,

if(j>=v[i])//当体积大于v[i]时才有能装i的机会
    f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);

我们在j大于v[i]时才进行操作,不如直接让j从v[i]枚举,这样我们就可以省去if循环,变成

for(int j=v[i];j<=m;j++)
    f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);

此时循环里的语句也要删去i维,如果我们直接删去i维,即

f[j]=max(f[j],f[j-v[i]]+w[i])

由于j是从小到大枚举,且j-v[i]<j,所以我们在枚举到j时,j-v[i]在之前肯定算过了
为什么一维情况下枚举背包容量需要逆序?

在二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i][j]与f[i - 1][j]是独立的。

而优化到一维后,如果我们还是正序,则有f[较小体积] 更新到 f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。

例如,一维状态第i轮对体积为3的物品进行决策,则f[7]由f[4]更新而来,这里的f[4]正确应该是f[i - 1][4],

但从小到大枚举j这里的f[4]在第i轮计算却变成了f[i][4]。

当逆序枚举背包容量j时,我们求f[7]同样由f[4]更新,但由于是逆序,这里的f[4]还没有在第i轮计算,所以此时实际计算的f[4]仍然是f[i - 1][4]。

简单来说,一维情况正序更新状态f[j]需要用到前面计算的状态已经被「使用过」,逆序则不会有这样的问题。所以对于j我们要从大到小遍历

代码

#include<iostream>
 
using namespace std;
 
const int N=1e4+10;
 
int n,m;
int v[N],w[N];
int f[N];
 
int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d %d",&v[i],&w[i]);

    for(int i=1;i<=n;i++)//枚举物品i
        for(int j=m;j>=v[i];j--)//从大到小枚举
            f[j]=max(f[j],f[j-v[i]]+w[i]);
    printf("%d",f[m]);
    return 0;
}

1.2 01背包求方案数

题目描述

题目链接:278. 数字组合 - AcWing题库

思路

和上述求最大值的思路基本相同,

对于本题我们可以把每个 正整数 看作是一个 物品

正整数的值就是物品的 体积

我们方案选择的目标是最终 体积 恰好为 m 时的方案数

注意要初始化,这里需要初始化f[0][0]=1,即在0个物品,值恰好为0时的方案数为1

二维代码

#include<iostream>

using namespace std;

const int N=110;

int n,m,f[N][100005];
int v[N];

int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&v[i]);
    f[0][0]=1;//当可选数字为0,和为0时也是一种
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=m;j++)//和恰好为j时
        {
            f[i][j]=f[i-1][j];//不选时
            if(v[i]<=j)
                f[i][j]+=f[i-1][j-v[i]];//选第i个时直接加上,选不选都是符合的方案
        }
    }
    printf("%d",f[n][m]);
    return 0;
}

一维代码

优化过程和01背包一样

#include<iostream>

using namespace std;

const int N=1e4+10;

int n,m,f[N],v[N];

int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&v[i]);
    f[0]=1;//当可选数字为0,和为0时也是一种
    for(int i=1;i<=n;i++)
        for(int j=m;j>=v[i];j--)
            f[j]+=f[j-v[i]];
    printf("%d",f[m]);
    return 0;
}

1.3 01背包——二维费用

题目描述

题目链接

8. 二维费用的背包问题 - AcWing题库

思路

每件物品只能 用一次 ,所以这是一个01背包模型

费用一共有两个,一个是 体积V,一个是 重量M,我们用2个维度来维护

代码

#include<bits/stdc++.h>

using namespace std;

const int N=1e3+10;

int n,m1,m2;//1是体积 2是重量
int f[N][N];//在前i种物品中,第一费用体积不超过j,第二费用重量不超过k的所有方案
int w[N],v1[N],v2[N];

int main()
{
    scanf("%d %d %d",&n,&m1,&m2);
    for(int i=1;i<=n;i++)
        scanf("%d %d %d",&v1[i],&v2[i],&w[i]);
    for(int i=1;i<=n;i++)
        for(int j=m1;j>=v1[i];j--)//枚举体积
            for(int k=m2;k>=v2[i];k--)//枚举重量
                f[j][k]=max(f[j][k],f[j-v1[i]][k-v2[i]]+w[i]);
    printf("%d",f[m1][m2]);
    return 0;
}

二、完全背包问题

2.1 完全背包求最大价值

题目描述

题目链接:3. 完全背包问题 - AcWing题库

思路

与01背包稍有不同,这里每个物品我们都可以选任意个

朴素版代码

#include<iostream>

using namespace std;

const int N=1e4+10;

int n,m;
int v[N],w[N];
int f[N][N];

int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d %d",&v[i],&w[i]);
    for(int i=1;i<=n;i++)
        for(int j=0;j<=m;j++)
            for(int k=0;k*v[i]<=j;k++)//体积不能大于j
                f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
    printf("%d",f[n][m]);
    return 0;
}

优化过程

我们观察 不选物品i的状态f[i.j]和 选1个物品i的状态f[i.j-v],看看它们都代表上一维中哪部分的最大值

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w ,  f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , ....., f[i-1,j-k*v]+k*w )
f[i , j-v]= max(            f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , ....., f[i-1,j-(k-1)*v]+(k-1)*w )

我们可以看出只有f[i][j]中的f[i-1][j]特殊,后面所有项,都可以通过f[i][j-v]+w实现。

由上两式,可得出如下递推关系: 

f[i][j]=max(f[i,j-v]+w , f[i-1][j]) 

所以不需要枚举数量k,优化后如下

for(int i=1;i<=n;i++)
    for(int j=0;j<=m;j++)
    {
        f[i][j]=f[i-1][j];
        if(j-v[i]>=0)
            f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
    }

此时和01背包的代码基本相同

for(int i=1;i<=n;i++)
    for(int j=0;j<=m;j++)
    {
        f[i][j]=f[i-1][j];
        if(j-v[i]>=0)
            f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
    }

只有1句不同

f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包
f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题

那么可以像01背包那样优化:

for(int i=1;i<=n;i++)
    for(int j=v[i];j<=m;j++)//注意了,这里的j是从小到大枚举,和01背包不一样
            f[j]=max(f[j],f[j-v[i]]+w[i]);

这里为什么从小到大,还是有些不太清楚。后续有新的理解会更新。个人当前的理解是:01背包用的是第i-1维状态的值,而完全背包用的是第i维的值,01背包中,从大到小枚举是怕i-1维的值被i维的覆盖了,而完全背包需要的就是第i维的值,所以需要从小到大。

优化后代码

#include<iostream>
 
using namespace std;
 
const int N=1e4+10;
 
int n,m;
int v[N],w[N];
int f[N];
 
int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d %d",&v[i],&w[i]);
         
    for(int i=1;i<=n;i++)
        for(int j=v[i];j<=m;j++)//从小到大,与01背包问题不同
            f[j]=max(f[j],f[j-v[i]]+w[i]);
    printf("%d",f[m]);
    return 0;
}

2.2 完全背包求方案数

题目描述

题目链接:1023. 买书 - AcWing题库

思路

每种书我们可以看成1种 物品

书的价格 就是物品的 费用

每种书的 数目任意(不超过总钱数)

由此可知是完全背包问题

注意初始化!f[0][0]=1,不买也是一种方案

因为是完全背包,也能按照完全背包的方法优化

代码


//朴素版
#include<iostream>

using namespace std;

const int N=1e3+10;

int n;
int f[10][N];
int v[5]={0,10,20,50,100};

int main()
{
    scanf("%d",&n);
    f[0][0]=1;
    for(int i=1;i<=4;i++)
        for(int j=0;j<=n;j++)//前i种物品不超过j元的所有方案数
            for(int k=0;k*v[i]<=j;k++)//k个i
                f[i][j]+=f[i-1][j-k*v[i]];
    printf("%d",f[4][n]);
    return 0;
}

*/

//优化后
#include<iostream>

using namespace std;

const int N=1e3+10;

int n;
int f[N];
int v[5]={0,10,20,50,100};

int main()
{
    scanf("%d",&n);
    f[0]=1;
    for(int i=1;i<=4;i++)
        for(int j=v[i];j<=n;j++)
                f[j]+=f[j-v[i]];
    printf("%d",f[n]);
    return 0;
}

2.2 完全背包——二维费用

题目描述

思路

代码

三、多重背包问题

3.1 多重背包求最大价值

题目描述

题目链接:4. 多重背包问题 I - AcWing题库

思路

n种物品,每种我们最多选s个,总体积不超过m。与完全背包基本一样,就是限制了个数。

朴素版代码

#include<iostream>
#include<algorithm>
 
using namespace std;
 
const int N=1e4+10;//n*m
 
int n,m,v[N],w[N],s[N];
int f[N][N];
 
int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d %d %d",&v[i],&w[i],&s[i]);
         
    for(int i=1;i<=n;i++)
        for(int j=0;j<=m;j++)
            for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
                f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+k*w[i]);
    printf("%d",f[n][m]);
    return 0;
}

优化过程

由于和完全背包很像,这里我们试试能不能像完全背包一样优化

观察f[i][j]和发f[i][j-v]:

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w ,  f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....f[i-1,j-s*v]+s*w)
f[i , j-v]= max(            f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....f[i-1,j-s*v]+(s-1)*w,f[i-1,j-(s+1)*v]+s*w)

我们发现由于多了一项f[i-1,j-(s+1)*v]+s*w不符合规律,不能像完全背包那样优化。

我们每次都需要遍历一遍物品i的数量,可以从这方面入手,可以用2进制来优化一下数量表示。

对于s个,我们可以把s硬拆,用2进制表示,

如s=1023,我们可以拆为1,2,4,8,16,……512

每个依次选或者不选,就可以组成0~1023中的每一个数,这是2^n -1类的数

再如一个普通的数s=200,我们可以拆成1,2,4,8,16,32,64,73,应该加起来刚好为s。

这样就可以转化为一个01背包问题,我们对拆开的数字依次选或者不选,也能组成每一个数,并不会有遗漏情况。

这样就把O(n)优化为O(logn)。

优化后代码

#include<iostream>
#include<algorithm>
 
using namespace std;
 
const int N=1e4+10;//n*m
 
int n,m,v[N],w[N],f[N];
 
int main()
{
    scanf("%d %d",&n,&m);
    int cnt=0;//cnt写外面就是为了让后面输入的v[i],w[i]不把前面的顶掉
    for(int i=1;i<=n;i++)
    {
        int a,b,s;//体积a,价值b,个数s
        scanf("%d %d %d",&a,&b,&s);//s个
         
        int k=1;//记录个数
        while(k<=s)//按照1 2 4 8等二进制表示方法将个数s拆分
        {
            cnt++;
            v[cnt]=k*a;//k个物品的体积
            w[cnt]=k*b;//k个物品的价值
            s-=k;
            k*=2;
        }
        if(s)//如果有剩余,另加一个表示   
        {
            cnt++;
            v[cnt]=s*a;
            w[cnt]=s*b;
        }
         
    }
    n=cnt;//现在的个数就是拆分后的个数,拆分完之后就是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]);
    printf("%d",f[m]);
    return 0;
}

3.2 多重背包求方案数

题目描述

题目链接:P1077 [NOIP2012 普及组] 摆花 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路

n种花,每种花可以看成一种 物品 

每盆花的体积为1

每种花最多选ai盆,有上限

可以看出是多重背包问题

记得初始化:f[0][0]=1

代码

#include<iostream>
 
using namespace std;
 
const int N=110;
const int mod=1e6+7;
typedef long long ll;
 
int n,m,s[N];//s代表个数上限 
ll f[N][N];
 
int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&s[i]);
    f[0][0]=1;
    for(int i=1;i<=n;i++)
        for(int j=0;j<=m;j++)
            for(int k=0;k<=min(s[i],j);k++)
                f[i][j]=(f[i][j]+f[i-1][j-k])%mod;
    printf("%lld",f[n][m]%mod);
    return 0;
}

3.1 多重背包——二维费用

题目描述

思路

代码

四、分组背包问题

4.1 分组背包求最大值

题目描述

题目链接:9. 分组背包问题 - AcWing题库

思路

分组背包问题,是我们有一个体积为m的背包,给定了在n组物品中,我们在每组中至多选择一个(也可以不选),求最大价值。

我们只需要用二维数组分别记下体积和价值,选的过程和01背包差不多。只是多一维k表示我们选的第k个

朴素版代码

#include<iostream>
#include<algorithm>
 
using namespace std;
 
const int N=110;
int n,m,s[N];
int v[N][N],w[N][N];
int f[N][N];
 
int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)//n组
    {
        scanf("%d",&s[i]);
        for(int j=1;j<=s[i];j++)
            scanf("%d %d",&v[i][j],&w[i][j]);
    }
     
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=m;j++)
        {
            f[i][j]=f[i-1][j];//不选i
            for(int k=0;k<=s[i];k++)//选第k个
                if(j>=v[i][k])
                    f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
        }
    }
 
    printf("%d",f[n][m]);
    return 0;
}

优化过程

按照01背包逆向枚举体积来优化

优化后代码

#include<iostream>
using namespace std;
const int N=110;
int n,m;
int v[N][N],w[N][N],s[N];
int f[N];
 
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&s[i]);
        for(int j=1;j<=s[i];j++)
            scanf("%d %d",&v[i][j],&w[i][j]);
    }
    for(int i=1;i<=n;i++)
        for(int j=m;j>=0;j--)
            for(int k=0;k<=s[i];k++)
                if(j>=v[i][k])
                    f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
    printf("%d",f[m]);
    return 0;
}

五、背包路径问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值