细谈多重背包问题

简介: 细谈多重背包问题

多重背包问题朴素解法

多重背包问题是背包问题的一种扩展,与0/1背包问题和分数背包问题有些不同。在多重背包问题中,每种物品都有限定的数量,不再是仅有一个,而是有多个。问题的描述如下:

给定一个背包容量为C,有n种物品,每种物品有重量w[i]、价值v[i]和数量s[i]。目标是选择物品放入背包,使得它们的总重量不超过背包容量,并且总价值最大。

这个问题在一些应用中更为真实,例如购物时某种商品有多件可供选择。解决多重背包问题的方法通常是在0/1背包问题的基础上进行一些调整。

经典解法就是三重for循环,一层枚举物品个数,二层枚举背包容量(逆序),三层枚举物品个数。

#include<stdio.h>
int dp[6001],w[501],v[501],nums[501];//dp[i]表示背包容量为i时所获得最大价值
int n,m;
int max(int x,int y){
  return x>y?x:y;
}
int main(){
  scanf("%d%d",&n,&m);
  for(int i=1;i<=n;i++){
    scanf("%d%d%d",&v[i],&w[i],&nums[i]);
  }
  for(int i=1;i<=n;i++){//枚举物品个数
    for(int j=m;j>=1;j--){//逆序枚举背包容量,01背包的扩展
      for(int k=0;k<=nums[i];k++){//枚举物品个数
        if(j>=k*v[i]){//如果背包容量大于此时物品重量
          dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);//可以选择,并且判断选还是不选最优
        }
      }
    }
  }
  printf("%d",dp[m]);
  return 0;
}

时间复杂度有着三重for循环O(nms),时间复杂度挺高了,有没有优化一点的解法,那肯定是有的,利用二进制优化


二进制优化解法

多重背包问题的二进制优化是一种常用的优化方法,它将多个相同的物品拆分成若干份,每份数量为2^k个。这样,问题就转化为了一个0/1背包问题,可以用0/1背包问题的解法来处理。

具体步骤如下:


1.拆分物品: 对于每种物品i,如果s[i]小于等于2^k,直接将这s[i]个物品当作一个整体处理。否则,拆分成若干份,每份数量为2^k,直到处理完所有数量。

2.转化为0/1背包问题: 将每个拆分后的子物品视为一个新的物品,其重量和价值分别为原物品的重量和价值乘以拆分的数量。这样,问题就被转化为一个0/1背包问题。

3.求解0/1背包问题: 使用动态规划等方法来解决新的0/1背包问题。

4.合并解: 将得到的解合并起来,得到原多重背包问题的解。

这种方法的优点在于将多重背包问题转化为了0/1背包问题,利用了0/1背包问题的解法,同时减小了问题规模。这对于规模较大的问题可以提高求解效率。

这个问题的动态规划状态转移方程和处理方法会因为引入了数量而有所不同,但基本思路仍然是在原问题的基础上进行扩展。


主要利用了二进制特点,由2的k次方可以表示任意数,比如:1,2,4,8,16……;我想表示5,可以取一个1一个4表示。

#include<iostream>
using namespace std;
int dp[1001],w[1001],c[1001];
int n,m,t;
int a,b,nums;
int main(){
  cin>>n>>m;
  for(int i=1;i<=n;i++){
    cin>>a>>b>>nums;
    for(int j=1;j<=nums;j<<=1){//这下面就是二进制分解
      w[t]=j*a;//把num分解成1,2,4,8……乘上对应的重量和价值
      c[t++]=j*b;
      nums-=j;//分解出来就要减去
    }
    if(nums){//如果num还有剩余,让剩余的自己单独成一项
      w[t]=nums*a;
      c[t++]=nums*b;
      nums=0;//剩余的自成一项,用完了就是0了
    }
  }
  for(int i=0;i<t;i++){//下面就是01背包解法了
    for(int j=m;j>=w[i];j--){
      dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
    }
  }
  printf("%d",dp[m]);
  return 0;
}

时间复杂度O(nmlogs),对于一些提高题来说,可能还过不了,那就再优化一下,单调队列优化。


单调队列优化解法

多重背包问题的单调队列优化解法是一种高效的解法,它通过维护一个单调队列来降低动态规划的时间复杂度。这个方法的核心思想是通过队列保存物品的信息,以减少重复计算。

以下是单调队列优化解决多重背包问题的基本步骤:


1.将每种物品展开: 将每种物品按照数量展开成若干个单独的物品。这样,问题就转化为了一个普通的0/1背包问题。

2.使用单调队列: 对于每个物品,维护一个单调递减的队列,队列中存储的是物品的编号。队列头部的物品拥有最大的价值。

3.动态规划: 使用动态规划的思想来更新队列。遍历背包容量范围,对于每一个容量,从队列头部取出物品,更新当前状态的最大价值。然后将当前物品放回队列,保持队列的单调性。

4.重复处理: 重复处理每个物品,直到处理完所有物品。


这种方法的优势在于通过单调队列的维护,避免了对于相同子问题的重复计算,从而提高了算法的效率。在处理大规模多重背包问题时,单调队列优化是一个有效的解决方案。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e3+5,M=2e4+5;
int f[M],g[M],v[N],w[N],s[N],q[M];
int n,m;
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
      memcpy(g,f,sizeof(f));//因为要正序遍历背包容量了,复制一个f数组g,保证在遍历i+1个物品时,状态由g[i]转移过来
        cin>>v[i]>>w[i]>>s[i];
        for(int j=0;j<v[i];j++)//分解成v[i]个类
        {
            int head=0,tail=-1;
            for(int k=j;k<=m;k+=v[i])//对每个类使用单调队列,s[i]的数就是滑动窗口的宽度
            {
                if(head<=tail&&q[head]<k-s[i]*v[i])head++;//q[h]不在窗口[k-s[i]*v[i],k-v[i]]之内,在其左侧,队头要出队
                if(head<=tail)f[k]=max(g[k],g[q[head]]+(k-q[head])/v[i]*w[i]);//使用队头最大值更新f,(k-q[head])/v[i]表示还能放入的物品个数
                //f[k]通过前面的旧值g[q[head]]来更新,所以窗口在g数组上滑动,f[k]=窗口中最大值+还能放入物品的价值
                while(head<=tail&&g[k]>=g[q[tail]]+(k-q[tail])/v[i]*w[i])tail--;//当前值大于队尾值,队尾出队
                q[++tail]=k;//下标入队,便于对头出队
            }
        }
    }
    cout<<f[m]<<endl;
    return 0;
}

时间复杂度O(nm),时间复杂度大大降低,笔者认为单调队列优化比较难懂,很抽象,多看几遍,可以用视频辅助学习,笔者从B站找了两个讲的比较好的视频。

E13 背包DP 多重背包 单调队列优化_哔哩哔哩_bilibili

多重背包——单调队列优化_哔哩哔哩_bilibili

由于笔者水平有限,理解的不是很透彻,写得也不是很好,一些方面可能也存在着问题,望大家理解支持,有错误就指出改正,大家一起进步,下篇更新完全背包问题

相关文章
|
Serverless
MATLAB-常见插值方法及常见知识
MATLAB-常见插值方法及常见知识
966 0
MATLAB-常见插值方法及常见知识
|
10月前
|
机器学习/深度学习 算法 5G
【MUSIC、最大似然与克拉美-罗下界】MUSIC与ESPRIT 算法来估计到达角(AoA),并尝试推导克拉美-罗下界(CRLB)以分析其性能研究(Matlab代码实现)
【MUSIC、最大似然与克拉美-罗下界】MUSIC与ESPRIT 算法来估计到达角(AoA),并尝试推导克拉美-罗下界(CRLB)以分析其性能研究(Matlab代码实现)
695 0
|
机器学习/深度学习 算法 数据处理
SVM的优缺点是什么
SVM的优缺点是什么
1588 9
|
存储 算法 Linux
【c++】STL简介
本文介绍了C++标准模板库(STL)的基本概念、组成部分及学习方法,强调了STL在提高编程效率和代码复用性方面的重要性。文章详细解析了STL的六大组件:容器、算法、迭代器、仿函数、配接器和空间配置器,并提出了学习STL的三个层次,旨在帮助读者深入理解和掌握STL。
775 0
|
存储
如何理解结构体的浅拷贝与深拷贝
结构体的浅拷贝仅复制对象的引用或基本数据类型值,不创建新对象;深拷贝则会递归地复制所有对象及其引用的对象,形成完全独立的新对象。两者主要区别在于是否共享内部对象。
|
存储 人工智能 算法
【算法总结】拓扑排序
【算法总结】拓扑排序
1316 0
|
算法 iOS开发 C++
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); 的作用和注意事项
默认情况下,C++的输入输出流与C标准库的输入输出函数是同步的,这会造成一定的性能损失。:使用cin.tie(0)和cout.tie(0)可以取消cin与cout之间的绑定,这意味着在进行输入操作时,不需要强行刷新输出缓冲区。:如果你的程序在输入输出中同时使用了C++的输入输出流和C标准库的输入输出函数(如scanf和printf),则不应该使用这段代码。:在使用了这段代码后,应避免使用C标准库的输入输出函数(如printf和scanf),因为这些函数与输入输出流的同步已被关闭。这段代码的主要用途是。
2142 1
|
自然语言处理 PyTorch TensorFlow
Transformers 4.37 中文文档(一)(2)
Transformers 4.37 中文文档(一)
565 1
|
自然语言处理 PyTorch TensorFlow
Transformers 4.37 中文文档(一)(1)
Transformers 4.37 中文文档(一)
534 1