写这篇文章主要是为了帮帮新人吧,dalao勿喷.qwq算法
每种物品都有一个价值w和体积c.//这个就是下面的变量名,请看清再往下看.数组
你如今有一个背包容积为V,你想用一些物品装背包使得物品总价值最大.函数
多种物品,每种物品只有一个.求能得到的最大总价值.post
咱们考虑是否选择第i件物品时,是须要考虑前i-1件物品对答案的贡献的.优化
若是咱们不选择第i件物品,那咱们就至关因而用i-1件物品,填充了体积为v的背包所获得的最优解.ui
而咱们选择第i件物品的时候,咱们要获得体积为v的背包,咱们须要经过填充用i-1件物品填充获得的体积为v-c[i]的背包获得体积为v的背包.spa
//请保证理解了上面加粗的字再往下看.code
因此根据上面的分析,咱们很容易设出01背包的二维状态blog
\(f[i][v]\)表明用i件物品填充为体积为v的背包获得的最大价值.队列
从而很容易的写出状态转移方程
\(f[i][v]=max(f[i-1][v],f[i-1][v-c[i]]+w[i])\)
对于当前第\(i\)件物品,咱们须要考虑其是否能让咱们获得更优解.
显然,根据上面的话
咱们选择第i件物品的时候,咱们要获得体积为v的背包,咱们须要经过填充用i-1件物品填充获得的体积为v-c[i]的背包获得体积为v的背包.
咱们须要考虑到\(v-c[i]\)的状况.
当不选当前第\(i\)件物品的时候,就对应了状态转移方程中的\(f[i-1][v]\),
而选择的时候就对应了\(f[i-1][v-c[i]]+w[i]\).
咱们考虑一个问题.
若是一个体积为5的物品价值为10,而还有一个体积为3的物品价值为12,一个体积为2的物品价值为8.显然咱们会选择后者.
这样咱们的状态转移方程中就不必定会选择i物品。
其实最好地去理解背包问题的话,仍是手跑一下这个过程,会加深理解。
代码写法↓
for(int i=1;i<=n;i++)//枚举 物品 for(int j=1;j<=V;j++)//枚举体积 if(j>=c[i]) f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+w[i]);//状态转移方程. else f[i][j]=f[i-1][j]. //上面的if语句是判断当前容量的背包可否被较小体积的背包填充获得. //显然 若是j-c[i]<0咱们没法填充 //(谁家背包负的体积啊 (#`O′)
可是二维下的01背包们仍是没法知足,怎么办?
考虑一维如何写!
仔细观察会发现,二维状态中,咱们的状态每次都会传递给i(就是说咱们的前几行会变得没用.)
这就给了咱们写一维dp的机会啊
因此咱们理所固然地设状态\(f[i]\)表明体积为i的时候所能获得的最大价值.
容易发现的是,咱们的\(f[i]\)只会被i之前的状态影响.
若是咱们顺序枚举,咱们的\(f[i]\)可能被前面的状态影响.
因此咱们考虑倒叙枚举,这样咱们的\(f[i]\)不会被i之前的状态影响,而咱们更新的话也不会影响其余位置的状态.
(能够手绘一下这个过程,应该不是很难理解.)
或者来这里看看(可能图画的有点丑了
代码写法↓
for(int i=1;i<=n;i++)//枚举 物品 for(int j=V;j>=c[i];j--)//枚举体积 f[j]=max(f[j],f[j-c[i]]+w[i]);//状态转移方程.
//应该不是很难理解.
01背包问题是背包问题中最基础,也是最典型的问题.其状态转移方程也是基础,更能够演变成其余背包的问题.
请保证看懂以后再向下看.
例题-->p1048 采药
此类背包问题中,咱们的每种物品有无限多个,可重复选取.
相似于01背包,咱们依旧须要考虑前i-1件物品的影响.
此时咱们依旧能够设得二维状态
\(f[i][v]\)表明用i件物品填充为体积为v的背包获得的最大价值
依旧很容易写出状态转移方程
\(f[i][v]=max(f[i-1][v],f[i-1][j-k*c[i]]+k*w[i])\)
//其中k是咱们须要枚举的物品件数.而咱们最多选取\(\left\lfloor\frac{V}{c[i]}\right\rfloor\)个(这个应该不用解释
code
for(int i=1;i<=n;i++)//枚举物品 for(int k=1;k<=V/c[i];k++)//咱们的物品最多只能放件. for(int j=1;j<=t;j++) { if(c[i]*k<=j) f[i][j]=max(f[i-1][j],f[i-1][j-k*c[i]]+k*w[i]); else f[i][j]=f[i-1][j]; //判断条件与01背包相同. }
一样地,咱们去考虑一维状态(鬼才会考虑
依旧设
\(f[i]\)表明体积为i的时候所能获得的最大价值
与01背包不一样的是,咱们能够重复选取同一件物品.
此时,咱们就须要考虑到前面i-1件物品中是否有已经选取过(其实不必
即,咱们当前选取的物品,可能以前已经选取过.咱们须要考虑以前物品对答案的贡献.
所以咱们须要顺序枚举.
与01背包一维的写法相似.
代码写法↓
code
for(int i=1;i<=n;i++)//枚举物品 for(int j=c[i];j<=V;j++)//枚举体积.注意这里是顺序/ f[j]=max(f[j,f[j-c[i]]]+w[i]);//状态转移.
若是仍是不理解,来这里看看.(就是上面那个链接)
彻底背包也是相似于01背包,应该也算上是它的一种变形.
比较通常的写法是一维写法,但愿你们能掌握.
例题-->p1616 疯狂的采药
此类问题与前两种背包问题不一样的是,
这里的物品是有个数限制的.
(下面用\(num[i]\)表示物品i的个数.
咱们能够枚举物品个数,也能够二进制拆分打包
一样,咱们最多能够放\(\left\lfloor\frac{V}{c[i]}\right\rfloor\),但咱们的物品数量可能不够这么多.
所以,咱们枚举的物品个数是\(min(\left\lfloor\frac{V}{c[i]}\right\rfloor,num[i])\)
(其实没这么麻烦的,直接枚举到\(num[i]\)便可)
多个物品,咱们就能够当作为一个大的物品,再去跑01背包便可.
所以这个大物品的价值为\(k\)×\(w[i]\),体积为\(k\)×\(c[i]\)
code
for(int i=1;i<=n;i++)//枚举物品 for(int j=V;j>=0;j--)//枚举体积 for(int k=1;k<=num[i],k++) //这个枚举到num[i]更省心 if(j-k*c[i]>=0)//判断可否装下. f[j]=max(f[j],f[j-k*c[i]]+k*w[i]);
其实还能够对每种物品的个物品跑01背包问题,效率特别低
这里也给出代码
code
for(int i=1;i<=n;i++) for(int k=1;k<=num[i];k++) for(int j=V;j>=c[i];j--) f[j]=max(f[j],f[j-c[i]]+w[i]);
可是此类问题,咱们的通常解法倒是
二进制拆分的原理
咱们能够用 \(1,2,4,8...2^n\) 表示出\(1\) 到 \(2^{n+1}-1\)的全部数.
考虑咱们的二进制表示一个数。
根据等比数列求和,咱们很容易知道咱们获得的数最大就是\(2^{n+1}-1\)
而咱们某一个数用二进制来表示的话,每一位上表明的数都是\(2\)的次幂.
就连奇数也能够,例如->\(19\)能够表示为\(10010_{(2)}\)
这个原理的话应该很好理解,若是实在理解不了的话,仍是动手试一试,说服本身相信这一原理.
二进制拆分的作法
由于咱们的二进制表示法能够表示从\(1\)到\(num[i]\)的全部数,咱们对其进行拆分,就获得好多个大物品(这里的大物品表明多个这样的物品打包获得的一个大物品).
(简单来说,咱们能够用一个大物品表明\(1,2,4,8..\)件物品的和。)
而这些大物品又能够根据上面的原理表示出其余不是2的次幂的物品的和.
所以这样的作法是可行的.
咱们又获得了多个大物品,因此再去跑01背包便可.
这里只给出拆分部分的代码,相信你能够码出01背包的代码.
code
for(int i=1;i<=n;i++) { for(int j=1;j<=num[i];j<<=1) //二进制每一位枚举. //注意要从小到大拆分 { num[i]-=j;//减去拆分出来的 new_c[++tot]=j*c[i];//合成一个大的物品的体积 new_w[tot]=j*w[i];//合成一个大的物品的价值 } if(num[i])//判断是否会有余下的部分. //就好像咱们某一件物品为13,显然拆成二进制为1,2,4. //咱们余出来的部分为6,因此须要再来一份. { new_c[++tot]=num[i]*c[i]; new_w[tot]=num[i]*w[i]; num[i]=0; } }
时间复杂度分析
咱们拆分一种物品的时间复杂度为\(log(num[i])\).
咱们总共会有n种物品,再配上枚举体积的时间复杂度.
所以,二进制拆分作法的时间复杂度为\(O(\sum_{i=1}^nlog(num[i])\)×\(V )\)
首先回想多重背包最普通的状态转移方程
\(f[i][j]=max(f[i-1][j],f[i-1][j-k*c[i]]+k*w[i])\)
其中\(k \in [1,min(\left\lfloor\frac{V}{c[i]}\right\rfloor,num[i])]\)
下面用 \(lim\)表示\(min(\left\lfloor\frac{V}{c[i]}\right\rfloor,num[i])\)
容易发现的是\(f[i][j-k*c[i]]\)会被\(f[i][j-(k+1)*c[i]]\)影响 (很明显吧
(咱们经过一件体积为\(c[i]\)的物品填充体积为\(j-(k+1)*c[i]\)的背包,会获得体积为\(j-k*c[i]\)的背包.)
概括来看的话
\(f[i][j]\)将会影响 \(f[i][j+k*c[i]]\) \((j+k*c[i]<=V)\)
\(c[i]=4\)
容易发现的是,同一颜色的格子,对\(c[i]\)取模获得的余数相同.
且,它们的差知足等差数列! (公差为\(c[i]\).
通项公式为 \(j=k*c[i]+\)取模获得的余数
因此咱们能够根据对\(c[i]\)取模获得的余数进行分组.
便可分为\(0,1,2,3{\dots}c[i]-1\) 共\(c[i]\)组
且每组之间的状态转移不互相影响.
(注意这里是组.相同颜色为一组
相同颜色的格子,位置靠后的格子,将受到位置靠前格子的影响.
//可是这样的话,咱们的格子会重复受到影响.(这里不打算深刻讨论 惧怕误人子弟
即\(f[9]\)可能受到\(f[5]\)的影响,也可能受到\(f[1]\)的影响
而\(f[5]\)也可能受到\(f[1]\)的影响.
因此咱们考虑将原始状态转移方程变形.
这里一些推导过程我会写的尽可能详细(我也知道看不懂有多难受. qwq
令d=c[i],a=j/c[i],b=j%c[i]
其中a为全选情况下的物品个数.
则\(j=a*d+b\)
则带入原始的状态转移方程中
\(j-k*d = a*d+b-k*d\) $= (a-k)*d+b $
咱们令\((a-k)=k^{'}\)
再回想咱们最原始的状态转移方程中第二状态 : \(f[i][j-k*c[i]]+k*w[i]\) 表明选择\(k\)个当前\(i\)物品.
根据单步容斥 :全选\(-\)不选=选.
所以 \(a-(a-k)=k\)
而前面咱们已经令\((a-k)=k^{'}\)
而咱们要求的状态也就变成了
\(f[i][j]=max(f[i-1][k^{'}*d+b]+a*w[i]-k^{'}*w[i])\)
而其中,咱们的\(a*w[i]\)为一个常量(由于a已知.)
因此咱们的要求的状态就变成了
\(f[i][j]=max(f[i-1][k^{'}*d+b]-k^{'}*w[i])+a*w[i]\)
根据咱们的
\(k \in [1,lim]\)
容易推知
\(k^{'} \in [a-k,a]\)
那么
当前的\(f[i][j]\)求解的就是为\(lim+1\)个数对应的\(f[i-1][k^{'}*d+b]-k^{'}*w[i]\)的最大值.
(之因此为\(lim+1\)个数,是包括当前这个\(j\),还有前面的物品数量.)
将\(f[i][j]\)前面全部的\(f[i-1][k^{'}*d+b]-k^{'}*w[i]\)放入一个队列.
那咱们的问题就是求这个最长为\(lim+1\)的队列的最大值+\(a*w[i]\).
所以咱们考虑到了单调队列优化( ? ?ω?? )?
(这里再也不对单调队列多说.这个题的题解中,有很多讲解此类算法的,若是不会的话仍是去看看再来看代码.-->p1886 滑动窗口
//相信你只要仔细看了上面的推导过程,理解下面的代码应该不是很难.
//可能不久的未来我会放一个加注释的代码(不是立flag.
//里面两个while应该是单调队列的通常套路.
//这里枚举的\(k\)就是\(k^{'}\).
code
for(int i=1;i<=n;i++)//枚举物品种类 { cin>>c[i]>>w[i]>>num[i];//c,w,num分别对应 体积,价值,个数 if(V/c[i] <num[i]) num[i]=V/c[i];//求lim for(int mo=0;mo<c[i];mo++)//枚举余数 { head=tail=0;//队列初始化 for(int k=0;k<=(V-mo)/c[i];k++) { int x=k; int y=f[k*c[i]+mo]-k*w[i]; while(head<tail && que[head].pos<k-num)head++;//限制长度 while(head<tail && que[tail-1].value<=y)tail--; que[tail].value=y,que[tail].pos=x; tail++; f[k*c[i]+mo]=que[head].value+k*w[i]; //加上k*w[i]的缘由: //咱们的单调队列维护的是前i-1种的状态最大值. //所以这里加上k*w[i]. } } }
这里只简单的进行一下分析.(其实我也不大会分析 qwq
咱们作一次单调队列的时间复杂度为\(O(n)\)
而对应的每次枚举体积为\(O(V)\)
所以总的时间复杂度为\(O(n*V)\)
多重背包的写法通常为二进制拆分.
单调队列写法有些超出noip范围,但时间复杂度更优,能掌握仍是尽可能去掌握.
拆分物品这种思想应该不算很难理解,这个是比较通常的写法.但愿你们能掌握.
若是仍是比较抽象,但愿你们能动笔尝试一下.
例题-->p1776 宝物筛选(这个题以前仍是个pj/tg-,后来居然蓝了 emmm
所谓的混合三种背包就是存在三种物品。
一种物品有无数个(彻底背包),一种物品有1个(01背包),一种物品有\(num[i]\)个(多重背包)
这个时候通常只须要判断是哪种背包便可,再对应地去选择dp方程求解便可.
送上一波伪代码
code
for(int i=1;i<=n;i++) { if(彻底背包) { for(int j=c[i];j<=V;j++) f[j]=max(f[j],f[j-c[i]]+w[i]); } else if(01背包) { for(int j=V;j>=c[i];j--) f[j]=max(f[j],f[j-c[i]]+w[i]); } else//不然就是彻底背包了 { for(int j=V;j>=0;j--) for(int k=1;k<=num[i];k++) if(j-k*c[i]>=0) f[j]=max(f[j],f[j-k*c[i]]+k*w[i]); } }
//彻底背包拆分的话,能够当作01背包来作.(提供思路
混合三种背包问题应该不是很难理解(若是前面三种背包你真正了解了的话.
结合起来考的话应该也不会不少.
(毕竟背包问题太水了
例题:(这个我真的没找到qwq
通常分组背包问题中,每组中只能选择一件物品.
状态你们都会设\(f[k][v]\)表明前k组物品构成体积为v的背包所能取得的最大价值和.
状态转移方程也很容易想.
\(f[k][v]=max(f[k-1][v],f[k-1][v-c[i]]+w[i])\)
可是咱们每组物品中只能选择一件物品.
//这个时候咱们就须要用到01背包倒叙枚举的思想.
code:
for(int i=1;i<=k;i++)//枚举组别 for(int j=V;j>=0;j--)//枚举体积 for(now=belong[i])//枚举第i组的物品. { if(j-c[i]>=0) f[i][j]=max(f[i-1][j],f[i-1][j-c[now]]+w[now]); else f[i][j]=f[i-1][j]; }
这类问题是01背包的演变,须要注意的位置就是咱们枚举体积要在枚举第i组的物品以前
(由于每组只能选一个!)
此类背包问题中。若是咱们想选择物品i的附件,那咱们必须选择物品i.
在[Noip2006]金明的预算方案这题中.引入了主件与附件的关系.
就比如你买电扇必须先买电.
一个主件和它的附件集合实际上对应于分组背包中的一个物品组.
每一个选择了主件又选择了若干附件的策略,对应这个物品组的中的一个物品.
(也就是说,咱们把'一个主件和它的附件集合'当作为了一个能得到的最大价值的物品.)
具体实现呢?
咱们的主件有一些附件伴随,咱们能够选择购买附件,也能够不购买附件.
(固然咱们也能够选择不购买主件.
当咱们选择一个主件的时候,咱们但愿获得的确定是最大价值.
如何作?
咱们能够先对附件集合跑一遍01背包,从而得到这一主件及其附件集合的最大的价值.
(或者是彻底背包,视状况而定.)
代码大体写法是这样的↓
(每一个物品体积为1,\(w[]\)表明价值.)
不敢保证正确性,不过通常都是这样写的qwq
for(int i=1;i<=n;i++)//枚举主件. { memset(g,0,sizeof g);//作01背包要初始化. for(now=belong[i])//枚举第i件物品的附件. { for(int j=V-1;j>=c[now];j--)//由于要先选择主件才能选择附件,因此咱们从V-1开始. { g[j]=max(g[j],g[j-1]+w[now]); } } g[V]=g[V-1]+w[i]; for(int j=V;j>=0;j--) for(int k=1;k<=V;k++)//此时至关于"打包" .. { if(j-k>=0) f[j]=max(f[j],f[j-k]+w[i]+g[k-1]); } } printf("%d",f[V]);
有一种状况,是主件的附件依旧有附件.(不会互相依赖.
对于这种依赖关系,咱们能够构出这样的图.
这种背包就是传说中的树形背包.
(树形dp的一种)(应该后面会有人讲)或者等我讲
这题就是一个典型的树形背包入门题
这类问题更是背包问题的延伸,咱们须要考虑的就是如何取到每个主件及其附件的集合中的最大值.而这就运用到了前面01背包.
例题-->p1064 金明的预算方案
前面两种背包问题,已经有了泛化物品的影子.
(哪里有啊!喂,话说这是什么鬼东西
先给出概念.
该类物品并无固定的体积和价值,而是它的价值随着你分配给它的体积而变化
其实这个能够抽象成一个函数图象.
在定义域0~V中,此类物品的价值随着分配给它的价值变化而变化.
(可能不是一次函数也不是二次函数.
毕竟我没有遇到过这种题
如今应该很容易理解.
有依赖的背包问题就有着这种泛化物品的思想.
若是对于某一个物品组,咱们分配给它的体积越大,显然它的价值越大.
最终咱们的答案就是全部对答案有贡献的物品组的和.(保证在限制范围内.
这些物品组被分配体积的大小的限制就是0~V.
总的限制也是0~V,因此这就能够抽象为
最终结果\(f(V)=max(h(l)+g(V-l))\)
(这只是一个抽象的解释,还须要具体问题具体分析.
随着水平的提升(反正窝很弱QAQ
出题人会更加考察选手的思惟.(话说有这种毒瘤出题人嘛qwq
下面讨论几种变化.根据背包九讲的顺序.
输出方案
对于一个背包问题,咱们已经获得了最大价值.
如今良心毒瘤出题人要求你输出选择物品的方案
分析
咱们如今须要考虑的是如何记录这个状态.
很明显记录每一个状态的最优值,是由状态转移方程的哪一项推出来的.
若是咱们知道了当前状态是由哪个状态推出来的,那咱们很容易的就能输出方案.
开数组\(g[i][v]\)记录状态\(f[i][v]\)是由状态转移方程哪一项推出.
//以01背包一维写法为例.
code
for(int i=1;i<=n;i++) { for(int j=V;j>=c[i];j--) { if(f[j]<f[j-c[i]]+w[i]) { f[j]=f[j-c[i]]+w[i]; g[i][j]=true;///选第i件物品 } else g[i][j]=false;///不选第i件物品 } }
输出
code
int T=V; for(int i=n;i>=1;i--) { if(g[i][T]) { printf("used %d",i); T-=c[i];//减去物品i的体积. } }
不敢保证正确性,不过通常都是这样写的qwq
再放一下状态转移方程.
\(f[i][v]=max(f[i-1][v],f[i-1][v-c[i]]+w[i]\)
\(f[j]=max(f[j],f[j-c[i]]+w[i])\)
二维状态能够省去g数组,只须要判断\(f[i][v]\)是等于\(f[i-1][v]\)仍是等于\(f[i-1][v-c[i]]+w[i]\)就能输出方案.
一维状态好像不能,我不会啊qwq
输出字典序较小的最优方案
感受sort一下就能够吧
根据原文叙述来看,是将物品逆序排列一下.
与上面输出方案的解法相同(倒叙枚举.
惟一须要判断的是:
当\(f[i][v]==f[i-1][v]\) 而且\(f[i][v]==f[i-1][v-c[i]]+w[i]\)的时候.
咱们要选择后面这个方案.由于这样选择的话,咱们会获得更小的字典序.(很明显吧
** 求次优解or第k优解 **
此类问题应该是比较难理解.
因此我会尽可能去详细地解释,qwq.
首先根据01背包的递推式:(这里按照一维数组来说)
(v[i]表明物品i的体积,w[i]表明物品i的价值).
\(f(j)\)=\(max\left(f(j),f(j-v[i])+w[i]\right)\)
很容易发现\(f(j)\)的大小只会与\(f(j)\)、\(f(j-v[i])+w[i]\)有关
咱们设\(f[i][k]\)表明体积为i的时候,第k优解的值.
则从\(f[i][1]\)...\(f[i][k]\)必定是一个单调的序列.
\(f[i][1]\)即表明体积为i的时候的最优解
很容易发现,咱们须要知道的是,可否经过使用某一物品填充其余体积的背包获得当前体积下的更优解.
咱们用体积为7价值为10的物品填充成体积为7的背包,获得的价值为10. 而咱们发现又能够经过一件体积为3价值为12的物品填充一个体积为4价值为6的背包获得价值为18. 此时咱们体积为7的背包能取得的最优解为18,次优解为10. 咱们发现,这个体积为4的背包还有次优解4(它可能被两个体积为2的物品填充.) 此时咱们能够经过这个次优解继续更新体积为7的背包. 最终结果为 18 16 10
所以咱们须要记录一个变量c1表示体积为j的时候的第c1优解可否被更新.
再去记录一个变量c2表示体积为j-v[i]的时候的第c2优解.
咱们能够用v[i]去填充j-v[i]的背包去获得体积为j的状况,并得到价值w[i]. 同理j-v[i]也能够被其余物品填充而得到价值. 此时,若是咱们使用的填充物不一样,咱们获得的价值就不一样.
这是一个刷表的过程(或者叫推表?
(这里引用一句话)
一个正确的状态转移方程的求解过程遍历了全部可用的策略,也就覆盖了问题的全部方案。
考虑到咱们的最优解可能变化,变化成次优解.只用一个二维数组\(f[i][k]\)来实现可能会很困难.
因此咱们引入了一个新数组\(now[]\)来记录当前状态的第几优解.
\(now[k]\)即表明当前体积为i的时候的第k优解.
所以最后咱们能够直接将\(now[]\)的值赋给\(f[i]\)数组
具体实现的话能够看看个人这篇文章
例题的话也就是这个(上面的文章是这题的题解.里面有详细解释.
主要参考-->dd大牛的《背包九讲》
强大的合伙人-->w_x_c_q
若是仍是有不懂的地方,但愿能够多多提问.
(毕竟我也不是个坏人,qwq)