动态规划基础题(背包专题第二课):完全背包

欢迎来到背包专题第二课——完全背包专题

这篇博客是在动态规划基础题(背包专题第一课):0-1背包的基础上讲解的,如果还没有学过0-1背包,请点击链接先学习0-1背包超详细版

虽然你已经知道0-1背包的演变过程了,但是在这边我还是要从头开始讲,这样做题会好做许多。

完全背包题目描述:

现在有一个背包容量为m,有n种物品,每种物品可以无限取,第i种物品的体积为w[i],价值为c[i],现在要去一些物品放到背包里,问那些物品的总价值是多少?

老套路——暴搜!

现在的暴搜时间复杂度更加爆炸了,不过......

还是有必要从头开始的,因为这里有一个大家以后做动态规划的题的时候可以用的套路,将会在每一节的最后讲述。

根据0-1背包的思路,我们要枚举每样物品的状态,在0-1背包中,每件物品只能取或不取,但是在完全背包中,物品是无限量的,枚举没有上限,怎么办呢?

简单,题目里还有一个条件,所选物品总体积不能超过背包总容量,所以只要枚举到背包装不下,就可以停下了。

在此,我要恭敬地献上代码:

#include <iostream>
using namespace std;
int w[1005],c[1005];
int solve(int n,int m){       //这边仍然要用倒序,因为回溯是少不了的
    if (n==0){                //整个的遍历结束了
        return 0;
    }
    if (m<0)return -2147483647;
    int x=solve(n-1,m);
    for (int i=1;i*w[n]<=m;i++){
        x=max(x,solve(n-1,m-(i*w[n]))+(c[n]*i));
    }
    return x;
}
int main() {
    int n,m;//n表示物品数量,m表示背包容量
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++){
        scanf("%d%d",&w[i],&c[i]);
    }
    printf("%d\n",solve(n,m));
    return 0;
}

我们可以发现,这个程序的时间复杂度为O(m2^{n}),实在是可以炸掉一条街,不过有一个好消息:

套路的\frac{1}{4}已收集!

套路的第一步:打一个暴搜出来找线索。

记忆化搜索

我们又又又又又可以发现,这个代码会有很多重复算的步骤,这里不像上次那样举样例了,大家知道就好。

现在,我们可以令f[i][j]代替solve(i,j)(套路老掉牙)让后再次算到solve(i,j)的时候,就只要从f[i][j]中取数就可以了。

现在,我要再次恭恭敬敬地在下面的空白处附上代码:

#include <iostream>
using namespace std;
int w[1005],c[1005];
int f[1005][1005];
int solve(int n,int m){
    if (n==0){
        return 0;
    }
    if (m<0)return -2147483647;
    if (f[n][n]!=-1)return f[n][m];
    f[n][m]=solve(n-1,m);
    for (int i=1;i*w[n]<=m;i++){
        f[n][m]=max(f[n][m],solve(n-1,m-(i*w[n]))+(c[n]*i));
    }
    return f[n][m];
}
int main() {
    memset(f,-1,sizeof(f));
    int n,m;
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++){
        scanf("%d%d",&w[i],&c[i]);
    }
    printf("%d\n",solve(n,m));
    return 0;
}

复杂度分析:

该程序时间复杂度分析:O(nm^{2})

该程序空间复杂度分析:O(n^{2})

嗯......

还可以,但是已经没法优化了,我们需要找找可以优化的,时间复杂度差不多的写法。

叮咚!

DP套路的\frac{2}{4}已经收集!

第二步:打出搜索的记忆化优化

关键性的一步:递归->递推

我们已经将暴搜改成了时间复杂度更低的记忆化优化后的递归算法,然而,我们却无法再继续优化其空间复杂度或者时间复杂度了,但是这样的时间和空间是一般的题目无法接受的,所以我们要改写成另外一种方式。

递归,它的另一个方向就是递推,递归和递推都是动态规划的一种方式,现在,我们将把一个递归的代码转化成一个递推的代码。

首先,我们要老老实实地转移,不改变时间复杂度和空间复杂度,一改就很容易错有可能只是对于我这种菜的不行的人

首先,我们不大幅改变f数组的意义,我们令f[i][j]表示循环到第i个物品为止,若背包大小为j,那么可以获得的最大价值是多少。

这次的转移方程先对难找,但是若根据0-1背包的转移方程,我们可以很容易地就推出状态转移方程,由于0-1背包中每个物品每个只能拿一次,所以就少了枚举个数的循环,现在只需要再加一个枚举个数的循环就可以了,然后再转移时,把体积和价值乘一乘就可以了。

代码:

#include <iostream>
using namespace std;
int w[1005],c[1005];
int f[1005][1005];
int main() {
    int n,m;
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++){
        scanf("%d%d",&w[i],&c[i]);
    }
    for (int i=1;i<=n;i++){
        for (int j=m;j>=w[i];j--){
            for (int k=1;k*w[i]<=j;k++)f[i][j]=max(f[i][j],f[i-1][j-w[i]*k]+c[i]*k);//明显的,k表示拿的物品的数量
        }
    }
    printf("%d\n",f[n][m]);
    return 0;
}

明显的,递推的代码长度会比递归短许多。

由于这是递归版的另一种写法,我就不分析时间复杂度和空间复杂度了

成功收集DP套路的\frac{3}{4}

第三步:根据记忆化搜索时记录的方式,定出合理的状态转移方程。

优化空间

前面说过,我们把递归写成递推不是因为我们闲着没事干,而是因为我们要优化时间与空间。

我们先优化比较容易优化的空间吧,0-1背包里面提到过,可以用滚动数组优化空间,虽然这是空完全背包,但本质还是一样的。

我就不细讲了,直接上代码,自己理解!

#include <iostream>
using namespace std;
int w[1005],c[1005];
int f[1005];
int main() {
    int n,m;
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++){
        scanf("%d%d",&w[i],&c[i]);
    }
    for (int i=1;i<=n;i++){
        for (int j=m;j>=w[i];j--){
            for (int k=1;k*w[i]<=j;k++)f[j]=max(f[j],f[j-w[i]*k]+c[i]*k);
        }
    }
    printf("%d\n",f[m]);
    return 0;
}

分析复杂度了:

时间复杂度:O(nm^{2})

空间复杂度:O(n)

我们成功地优化掉了一维空间

优化时间

优化方式我在0-1背包里不小心透露了出来,等会在那边加个链接。

你想,我们是在用滚动数组对吧。

比方说我们现在处理到剩余空间为5的情况,那么剩余空间为4的情况一定是已经处理好了的。

我们在0-1背包里面之所以要倒着来,就是因为我们一个物品只能取一次,而且我们是从剩余空间小的地方转移过来的,我们不能知道剩余空间小的那些状态有没有取过这个物品,所以我们必须从后面开始转移。

那我们再考虑一下,限制0-1背包正序的条件在完全背包中已经被粉碎了。

所以我们可以转移一个已经取过该物品的状态。

所以,我们可以把第二层循环正过来,这样就可以省掉一个第三重循环。

上代码吧:

#include <iostream>
using namespace std;
int w[1005],c[1005];
int f[1005];
int main() {
    int n,m;
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++){
        scanf("%d%d",&w[i],&c[i]);
    }
    for (int i=1;i<=n;i++){
        for (int j=w[i];j<=m;j++){    //正序遍历,允许转移已经取过的状态
            f[j]=max(f[j],f[j-w[i]]+c[i]);
        }
    }
    printf("%d\n",f[m]);
    return 0;
}

复杂度分析:

时间复杂度:O(nm)

空间复杂度:O(n)

OK,整个DP套路都集齐了,我们来整理一下:

第一步:写出暴力(递归)代码。

第二步:写出递归的记忆化搜索。

第三步:根据记忆化的f数组定义,定义出DP的状态。

第四步:优化空间和时间。

好了,我的讲解到此结束,求点赞,求收藏,求关注!

  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值