01背包问题,完全背包,多重背包详解(C++代码实现)

背包问题

参考链接:
背包九讲经典博客:dd大牛的《背包九讲》
参考视频(B站大神敲代码):背包九讲专题

三种 背包问题:

1.01背包问题(每种物品只能选一次
2.完全背包问题(每种物品都可以选无限次
3.多重背包问题(每种物品具有不同的选择上限

01背包问题

题目

(参考链接:https://www.acwing.com/problem/content/2/)

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi 。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤10000 0<vi,wi≤10000
输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

基本思路

01背包问题是所有背包问题的基础,之后的问题都可以在此基础之上变化,所以一定要理解清楚。尤其是对待不同问题,找出状态转移矩阵是解题的关键。

解法归纳:

文字描述
1.如果装不下当前物品,那么前i个物品的组合和前i-1个物品的最佳组合是一样的
2.如果装得下当前物品
假设一:装当前物品:在给当前物品预留了相应空间的情况下,前i-1个物品预留了空间的的最佳组合加上当前物品的价值就是总价值
假设二:不装当前物品,那么前i个物品的组合和前i-1个物品的最佳组合是一样的 取假设一和假设二中较大的价值,就是当前最佳价值的组合

符号表示
f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值
状态转移矩阵为:f[ i ][ v ] = max{ f[ i-1 ][ v ] , f[ i-1 ][ v-c[ i ] ]+w[ i ] }

具体实例

直接用公式表示可能会有些抽象,接下来用题目中的具体事例来演示过程

用一个二维数组来记录过程,其中
的范围为1~5,表示背包的容量
的范围为1~4,每行代表一个物品
按照从左到右,从上到下的顺序,记录得到的表格如下:

背包容量012345
1022222
2024666
3024668
4024668

代码实现

暴力解法,二维数组
时间复杂度:O(NV)
空间复杂度:O(NV)

#include<algorithm>
#include<iostream>

using namespace std;

//注意此处的N至少要比最大范围多1
const int N=1001;
int f[N][N];
int V[N];
int W[N];

int main()
{
    int n,v;
    cin>>n>>v;
    //存储输入
    for(int i=1;i<=n;i++)
        cin>>V[i]>>W[i];
     
    for(int i=1;i<=n;i++)
    {
    	//背包容量需从0,...,v
        for(int j=0;j<=v;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]);
        }
    }
    
    //最终的答案并不一定是f[n][v],而是f[n][0...v]的最大值
    int res=0;
    for(int j=0;j<=v;j++)
        res=max(res,f[n][j]);
  
    cout<<res<<endl;
    
    return 0;
}

优化空间复杂度,一维数组
时间复杂度:O(NV)
空间复杂度:O(V)

根据状态转移矩阵f[ i ][ v ] = max{ f[ i-1 ][ v ] , f[ i-1 ][ v-c[ i ] ]+w[ i ] } 可知:
计算 f[ i ][ 0,…,V ] 时,只需访问 f[ i-1 ][ 0,…,V ],即二维矩阵的第 i 行计算只依赖于第 i-1 行的数据,无需存储整个二维数组,用一维数组不断迭代即可实现,类似于斐波那契数列的优化解法。

新的转态转移矩阵:f[ v ] = max( f[ v ] , f[ v-c[ i ] ] + w[ i ] )
在代码的实现中采用倒序遍历可以实现此过程,由于更新f[ v ] 使用的是更小的f[v-…],所以
等式左边的f[ v ] 即为f[ i ][ v ](最新更新结果)
等式右边的f[ v ] 即为f[ i-1 ][ v ](上一次更新结果)

#include<algorithm>
#include<iostream>

using namespace std;

const int N=1001;

int f[N];
int V[N];
int W[N];

int main()
{
    int n,v;
    cin>>n>>v;
    
    for(int i=1;i<=n;i++)
        cin>>V[i]>>W[i];
    
    //简化版一维数组
    for(int i=1;i<=n;i++)
    {
    	//采用倒序遍历 0,...,v
        for(int j=v;j>=V[i];j--)
            f[j]=max(f[j],f[j-V[i]]+W[i]);
    }
   
    cout<<f[v]<<endl;
    
    return 0;
}

完全背包问题

题目

(参考链接:https://www.acwing.com/problem/content/3/)

有 N种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤10000
0<vi,wi≤10000
输入样例

4 5
1 2
2 4
3 4
4 5
输出样例:

10

基本思路

完全背包问题与01背包问题的关系非常巧妙,
条件上只是每个物品可以选一次和无数次的区别
在代码实现时,只需将倒序遍历改变为顺序遍历即可

01背包问题中要按照v=V…0的逆序来循环。
这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。

而完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v=0…V的顺序循环。这就是这个简单的程序为何成立的道理

此时用二维数组表示即为,
(可以从下方表格看出,最优解是选了5次第一个物品)

背包容量012345
10246810
20246810
30246810
40246810

代码实现

#include<iostream>
#include<algorithm>

using namespace std;

int f[1001];

int main()
{
    int N,V;
    cin>>N>>V;
    
    for(int i=0;i<N;i++)
    {
        int v,w;
        cin>>v>>w;
        
        //顺序遍历,0,...,V
        for(int j=v;j<=V;j++)
            f[j]=max(f[j],f[j-v]+w);
    }
    
    cout<<f[V]<<endl;
    
    return 0;
}

多重背包问题

题目

(参考链接:https://www.acwing.com/problem/content/4/)

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000
0<vi,wi,si≤1000
输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:

10

基本思路

最基本的思路为,最优解中可以包含 0,1,2,…,s 第 i 个物品
状态转移矩阵为:f[ i ][ v ] = max{ f[ i-1 ][ v ] , f[ i-1 ][ v- k * c[ i ] ]+k*w[ i ] }
(k=0,1,2,…,s)
空间复杂度为:O(V*∑s[i])

上述思路的方法是将第 i 个物品拆分为1,2, …,s,对应的质量与价值也乘以相应的倍数,每个不同倍数的物品 i 就是一个全新的物品,然后就转换为了01背包问题。继续按照该思路,是否有更加高效的拆分方法?

二进制思想
参考二进制数的表示方法,将每件物品的数量都可以用1,2,4,8,… 的2^k组合来表示。

例如13,则0~13范围内的所有数都可以用1,2,4,6,四个数来表示,其中6=13-(1+2+4)。这样13就由之前拆分为13个物品,简化为拆分4个物品。

空间复杂度:O(V*∑log n[i])

代码实现

暴力解法

#include<iostream>
#include<algorithm>

using namespace std;

int f[101];

int main()
{
    int N,V;
    cin>>N>>V;
    
    for(int i=0;i<N;i++)
    {
        int v,w,s;
        cin>>v>>w>>s;
        
        for(int j=V;j>=v;j--)
        {
        	//添加物品数量遍历层
            for(int k=1;k*v<=j && k<=s;k++)
                f[j]=max(f[j],f[j-k*v]+w*k);
        }
    }
    
    cout<<f[V]<<endl;
    
    return 0;
}

二进制解法

#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

int f[2001];

struct use
{
    int v;
    int w;
};

int main()
{
    int N,V;
    cin>>N>>V;
    
    vector<use> goods;
   
    for(int i=0;i<N;i++)
    {
        int v,w,s;
        cin>>v>>w>>s;
        
        //二进制法对商品数量拆分
        for(int k=1;k<=s;k*=2)
        {
            s=s-k;
            goods.push_back({k*v,k*w});
        }
        //s-∑2^k
        goods.push_back({s*v,s*w});
    }
    
    //转换为01背包问题
    for(auto good:goods)
    {
        for(int j=V;j>=good.v;j--)
            f[j]=max(f[j],f[j-good.v]+good.w);
    }
    
    cout<<f[V]<<endl;
    
    return 0;
}
  • 13
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值