多重背包问题朴素解法
多重背包问题是背包问题的一种扩展,与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
由于笔者水平有限,理解的不是很透彻,写得也不是很好,一些方面可能也存在着问题,望大家理解支持,有错误就指出改正,大家一起进步,下篇更新完全背包问题。