【算法】经典背包问题

作者:指针不指南吗
专栏:算法篇

🐾或许会很慢,但是不可以停下来🐾

acwing 背包问题——学习笔记

01背包、完全背包、多重背包、分组背包

引入Dp

Dp问题,先写出基本形式,然后优化,对代码进行等价变形

  • 下面是Dp问题的分析基本流程

在这里插入图片描述

1.01背包

问题描述:

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

第 i 件物品的体积是 v i v_i vi,价值是 w i w_i wi

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

输出最大价值。

(1)问题分析

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

在这里插入图片描述

(2)代码实现

①基础版——二维

  • f[i][j] 表示前 i 个物品,背包容量 j 下的最优解(最大价值)

  • 当前背包容量不够(j < v[i]),没得选,因此前 i 个物品最优解即为前 i−1 个物品最优解:f[i][j]=f[i-1][j]

  • 当前背包容量够,两种选择:第 i 个物品,放或者不放,取最大值:max(f[i - 1][j], f[i - 1][j - v[i]] + w[i])

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=1010; 
    
    int n,m; //n表示物品个数,m表示最大容量
    int v[N],w[N];  //v表示体积,w表示价值
    int f[N][N]; //f[i][j]表示前i个物体,前j个容量的最大价值
    
    int main()
    {
        cin>>n>>m;
    
        for(int i=1;i<=n;i++)
            cin>>v[i]>>w[i];
    
        //f[0][0~m] 表示0个物体,容量都是0,因为是全局变量,省略
    
        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]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
            }
    
        cout<<f[n][m]<<endl;
    
        return 0;
    }
    

②优化版——一维

  • 整个转移方程中对于 i 这一维,只用到了i -1, 所以不需要记录所有的f[i],只需要用单个变量记录即可(滚动数组),去除 f 数组的 i 这一维

  • j 使用逆序枚举。如果我们按照正序枚举背包容量 j,即从小到大枚举,那么在更新 f[i][j] 时,可能会使用到f[i][j-w[i]] 这个状态,其中 w[i] 表示第 i 个物品的重量。这相当于在容量为j-w[i] 的情况下再次放入物品 i,与题目要求的 0/1 背包问题不符。

  • 因此,为了避免每个物品多次被放入背包的情况,我们采用逆序枚举背包容量 j 的方式更新 DP 状态。在逆序枚举的过程中,我们保证 f[i][j]f[i][j-w[i]] 之前被更新,从而确保每个物品只会被放入背包一次。

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=1010; 
    
    int n,m; //n表示物品个数,m表示最大容量
    int v[N],w[N];  //v表示体积,w表示价值
    int 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;
    }
    

2.完全背包

问题描述:

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

第 i 种物品的体积是 v i v_i vi ,价值是 w i w_i wi

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

输出最大价值。

(1)问题分析

在这里插入图片描述

(2)代码实现

①基础版——二维/三重循环

  • 问题分析部分已经讲的很清楚了

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=1010; 
    
    int n,m; //n表示物品个数,m表示最大容量
    int v[N],w[N];  //v表示体积,w表示价值
    int f[N][N]; //f[i][j]表示前i个物体,前j个容量的最大价值
    
    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=0;j<=m;j++)
                for(int k=0;k*v[i]<=j;k++) //k表示第i个物品数量
                    f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
    
        cout<<f[n][m]<<endl;
    
        return 0;
    }
    

②优化版——一维/两重循环

  • 发现规律

    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,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][j]=max(f[i,j-v]+w,f[i-1][j]) 
    

    进行优化

    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背包进行对比

    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]) 完全背包问题

    并进一步优化

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

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=1010; 
    
    int n,m; //n表示物品个数,m表示最大容量
    int v[N],w[N];  //v表示体积,w表示价值
    int 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;
    }
    

3.多重背包

问题描述:

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

第 i 种物品 最多有 s i s_i si 件,每件体积是 v i v_i vi ,价值是 w i w_i wi

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

输出最大价值。

(1)问题分析

在这里插入图片描述

(2)代码实现

①基础版——二维/三重循环

  • 上面问题分析讲的很清楚

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=110;
    
    int n,m;
    int v[N],w[N],s[N];
    int f[N][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=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-k*v[i]]+k*w[i]);
        
        cout<<f[n][m]<<endl;
        
        return 0;
    }
    

②优化版—一维 /二进制

  • 当数据很大时, O ( n 3 ) O(n^3) O(n3) 会超时,进行二进制优化

    题解链接: AcWing 5. 二进制优化,它为什么正确,为什么合理,凭什么可以这样分?? - AcWing

    讲一下为什么二进制优化可以哈。

    题目的意思是某物品最多有s件,我们需要从所有的物品中选择若干件,使这个背包的价值最大。题目并没有说某物品一定需要选多少件出来,也没有说一共要选多少件出来。只是选择若干件,至于选几件,无所谓,但要保证价值最大。

    按照优化的策略某物品有s件,我们给其打包分成了好几个大的物品。

    第一个大物品是包含原来该物品的1件,第二个大物品是包含原来该物品的2件,第三个大物品是包含原来该物品的4件,第四个大物品是包含原来该物品的8件,…依次类推。此时我们就把所有的物品都重新进行了一个分类。

    原先每个物品最多s件,我们就把这个件数条件给消去了。取而代之的是,按照一定原先件数组合出来的新若干大物品。

    我们又已知按照我们划分成大物品进行搭配组合,一定能转化为原先的若干件小物品出来。

    并且选择某物品的最多件数,是不会超过原先该物品的s件。所以就转化为从下面这些若干件大物品中,选择能使背包容积最大大的情况下,价值最高。这个就是一个01问题。转自知名网友评论

    代码如下:

    #include<iostream>
    using namespace std;
    
    const int N = 12010, M = 2010;
    
    int n, m;
    int v[N], w[N]; //逐一枚举最大是N*logS
    int f[M]; // 体积<M
    
    int main()
    {
        cin >> n >> m;
        int cnt = 0; //分组的组别
        for(int i = 1;i <= n;i ++)
        {
            int a,b,s;
            cin >> a >> b >> s;
            int k = 1; // 组别里面的个数
            while(k<=s)
            {
                cnt ++ ; //组别先增加
                v[cnt] = a * k ; //整体体积
                w[cnt] = b * k; // 整体价值
                s -= k; // s要减小
                k *= 2; // 组别里的个数增加
            }
            //剩余的一组
            if(s>0)
            {
                cnt ++ ;
                v[cnt] = a*s; 
                w[cnt] = b*s;
            }
        }
    
        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]);
    
        cout << f[m] << endl;
        return 0;
    }
    

4.分组背包

题目描述:

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

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 v i j v_{ij} vij ,价值是 w i j w_{ij} wij ,其中 i 是组号,j 是组内编号。

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

输出最大价值。

(1)问题分析
在这里插入图片描述

(2)代码实现

①基础版——二维

  • 思路看上图

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=110;
    int f[N][N];  
    int v[N][N],w[N][N],s[N];  
    int n,m,k;
    
    int main(){
        cin>>n>>m;
        for(int i=1;i<=n;i++)
        {
            cin>>s[i];
            for(int j=0;j<s[i];j++)
            {
                cin>>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];  //不选
                for(int k=0;k<s[i];k++)
                {
                    if(j>=v[i][k])     
                        f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);  //01背包
                }
            }
        }
        cout<<f[n][m]<<endl;
    }
    
    

②优化版—一维

  • 含有01背包,进行一维优化

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=110;
    
    //使用的上一层的f,从大到小枚举体积,使用本层,从小到大枚举体积
    //使用上一层,保证我们算这个所用到的体积,还没有被计算过,所以是存的上一层的状态
    
    int n,m;
    int v[N][N],w[N][N],s[N];
    int f[N];
    
    int main()
    {
        cin>>n>>m;
    
        for(int i=1;i<=n;i++)
        {
            cin>>s[i];
            for(int j=0;j<s[i];j++)
                cin>>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(v[i][k]<=j)
                        f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
    
        cout<<f[m]<<endl;
    
        return 0;
    }
    

Alt

  • 11
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

指针不指南吗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值