细谈多重背包问题

简介: 细谈多重背包问题

多重背包问题朴素解法

多重背包问题是背包问题的一种扩展,与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

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

相关文章
|
5月前
|
机器学习/深度学习 算法 测试技术
青蛙跳台阶:我如何得知它是一道斐波那契数列题?——应用题破题“三板斧”(二)
这篇内容是关于解题策略的,主要介绍了在面对应用题时可以采用的“三板斧”方法:举例、模拟和找规律。通过一个走楼梯的例子详细解释了这三个步骤如何运用。首先,举例将抽象问题具体化,比如走5级台阶有几种方式。其次,通过模拟不同情况,例如思考走每一步的可能,发现递归或循环的模式。最后,通过找规律归纳出一般性的解决方案,这里发现走台阶问题与斐波那契数列相关。文章还提到了一个拓展案例——矩形覆盖问题,同样可以用类似的方法分析。总的来说,文章强调了解题过程中理解和转化问题的重要性,以及通过训练提升这方面能力。
43 0
|
5月前
|
C语言
青蛙跳台阶:我如何得知它是一道斐波那契数列题?——应用题破题“三板斧”(一)
这篇内容介绍了C语言学习中的经典例题——斐波那契数列,强调了解题思路的重要性。斐波那契数列是一个数列,其中每个数是前两个数的和。文章指出,很多类似题目,如“青蛙跳台阶”,实质上都在考察斐波那契数列的实现。文中提供了斐波那契数列的定义、代码实现和递归与非递归的思路,并鼓励读者从问题中分析出解题模型,而非直接套用已知方法。此外,还提及了一道相关题目“矩形覆盖问题”,以供进一步练习。
40 0
|
11月前
|
算法
代码随想录算法训练营第四十六天 | LeetCode 139. 单词拆分、多重背包、背包总结
代码随想录算法训练营第四十六天 | LeetCode 139. 单词拆分、多重背包、背包总结
71 1
|
存储 移动开发 算法
C++/PTA 关于深度优先搜索和逆序对的题应该不会很难吧这件事
背景知识 深度优先搜索与 DFS 序 深度优先搜索算法(DFS)是一种用于遍历或搜索树或图的算法。以下伪代码描述了在树 T 上进行深度优先搜索的过程
110 0
可能你已经刷了很多01背包的题,但是真的对01背包领悟透彻了吗?,看我这一篇,使君对01背包的理解更进一步【代码+图解+文字描述】
可能你已经刷了很多01背包的题,但是真的对01背包领悟透彻了吗?,看我这一篇,使君对01背包的理解更进一步【代码+图解+文字描述】
|
机器学习/深度学习 算法 索引
算法步步为营(1)-两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。可以按任意顺序返回答案。
80 0
|
存储 算法 vr&ar
算法步步为营(02)-两数之和
两个非空链表,表示两个非负整数。它们每位数字都是逆序存储,且每个节点只能存储一位数字。 将两个数相加,并以相同形式返回一个表示和的链表。除了数字 0 之外,这两个数都不会以 0 开头。
98 0
|
机器学习/深度学习
带你轻松拿捏N皇后问题
要求任何两个皇后不同行、不同列,也不在同一 条斜线上
131 0
带你轻松拿捏N皇后问题
|
算法 JavaScript 前端开发
日拱算法:解两道“杨辉三角”题
什么是“杨辉三角”,想必大家并不陌生~~ 在「杨辉三角」中,每个数是它左上方和右上方的数的和。
|
机器学习/深度学习 算法
<<算法很美>>——(六)——回溯算法(下)—N皇后问题
<<算法很美>>——(六)——回溯算法(下)—N皇后问题
<<算法很美>>——(六)——回溯算法(下)—N皇后问题