超级详细的背包问题解析(01,完全,多重)

一、0-1背包问题

问题描述

关键:每件物品只能使用一次

分析

「0-1 背包」不断对第 i 个物品的做出决策,「0-1」正好代表不选与选两种决定。

(1)二维实现

f[i][j] :前i个物品,背包容量为j下的最大价值。

N件物品,N次决策,每一次对第i件物品的决策,由之前的状态更新。

如何更新f[i][j]?

1)当前背包容量不够,无法选择当前物品 j<v[i] f[i][j]=f[i-1][j]

2)当前背包容量够,可以选择是否选当前物品 f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i])

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N=1e3+10;
int n,m,v[N],w[N],f[N][N];

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

(2)一维实现

由于f[i][j]实际上只依赖于f[i-1][j](也就是说只要i从1到n就可以模拟出这种滚动的状态),可以使用一维数组实现

注意:内层循环必须逆向遍历,否则会重复计算(变成完全背包问题)。

原因就在于:f[j]=max(f[j],f[j-v[i]]+w[i]),只会用较小下标的f值更新较大下标的f值。如果正向遍历,如果f[j-v[i]]已经用了物品i,而f[j]无法区分是否物品i已经被使用了,就会多次使用物品i,变成了完全背包问题。如果逆向更新,此时的f[j-v[i]]还没有更新,绝对没有使用物品i,就不会导致物品i被使用多次。

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N=1e3+10;
int n,m,v[N],w[N],f[N];

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[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]);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

二、完全背包问题

问题描述

关键:每件物品都有无限件可以使用。

分析

完全背包与0-1背包不同的点在于,物品是无限多的。

我们已经在0-1背包问题中讨论过,内层循环需要从大到小,这样保证每个物品最多用一次,而在完全背包中,则需要从小到大,这样每个物品就可以用多次了。

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N=1e3+10;
int n,m,v[N],w[N],f[N];

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[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]]+w[i]);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

三、多重背包问题

问题描述

关键:物品既不是1件也不是无限多,而是有限件,需要用一个数组s[N]来保存每种物品的件数。

分析

(1)朴素版本

这个时候,我们需要加一重循环来判断一种物品到底拿多少件,第二层循环还是要倒序保证物品不会多拿。

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N=1e2+10;
int n,m,v[N],w[N],f[N],s[N];

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[i]>>s[i];
    }
    for(int i=1;i<=n;i++){
        for(int j=m;j>=v[i];j--){
            for(int k=0;k*v[i]<=j&&k<=s[i];k++){
                f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
            }
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

(2)改进版本

上面的代码时间复杂度是O(n^3),对于n比较大的题是过不了的,那么怎样才能够改进呢?

那就是对物品进行拆分,如果暴力拆,将物品拆为0-1背包的形式,就可以降到O(n^2)。但是如果物品数量特别多,将导致n暴增。

有没有更好的解决方案呢?

那就是:任何整数c可以表示为若干 2的幂次之和,通过组合这些幂次的数,可以覆盖0到c的所有可能取值。

  • 假设c=13,可拆分为 1,2,4,6(因为 1+2+4+6=13)。

  • 通过选/不选这些拆分后的数,可以组合出 0∼13之间的任意数量(如 5=1+4)。

剩下的问题就套用0-1背包的模板解决即可。

n<=1000,s<=2000 N需要开到n*log2s(向上取整)

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N=2e4+10;
int n,m,v[N],w[N],f[N],cnt;

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int a,b,c;
        cin>>a>>b>>c;
        int t=1;
        while(t<=c){
            v[++cnt]=t*a;
            w[cnt]=t*b;
            c-=t;
            t*=2;
        }
        if(c){
            v[++cnt]=c*a;
            w[cnt]=c*b;
        }
    }
    n=cnt;
    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]);
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

到这里,背包问题彻底结束,恭喜🎉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值