本咸鱼终于停课了,在Mr.pan的支持下膜了一波背包九讲,为了方便自己以后复习,将先在的一些感悟记录下来。
1.01背包
这种背包是最基础的背包,题目大概是这样:
有 N 件物品和一个容量为 V 的背包。放入第 i 件物品耗费的费用是 Ci,得到的 价值是 Wi。求解将哪些物品装入背包可使价值总和最大。
这个最初始的状态就是f[i][j],f[i][j]表示的是装到第i件物品(无论是否装入),使用了容量为j的背包(无论有没有装满)的时候,所能获得的最大价值。这样的话状态转移方程式就不难想出了f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+w[i])。外层再套一个双重循环,枚举i,j就可以了。代码如下
for(i=1;i<=n;i++) { for(j=c[i];j<=m;j++) { f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+w[i]); } }
然后,我们在求f[i][j]时,只使用了f[i-1][0~(j-c[i])]中的值,然而当我们需要求f[i+1][...]时又只需要使用f[i][...]中的值,所以我们就可以只把i开到2,循环使用数组,代码也就变成了这样:
int i,j,k=0; for(i=1;i<=n;i++) { k=k^1; for(j=1;j<=c[i];j++) { f[k][j]=max(f[k^1][j],f[k^1][j-c[i]]+w[i]); } }
虽然没有节省时间,但节省了非常多的空间。
背包九讲中还提供了一种优化空间的方法,也就是f数组只开一维,然后枚举j的时候从m到c[i]枚举,这样就防止了多次选取这件物品,具体代码如下:
for(i=1;i<=n;i++) { for(j=m;j>=c[i];j--) { f[j]=max(f[j],f[j-c[i]]+w[i]); } }
然而背包九讲上还提供了一种优化常数的方法,只不过本咸鱼没有看懂,等看懂后补上吧。
2.完全背包
这种背包跟0背包唯一的区别就在于每件物品可以无限使用。这样的话状态可以沿用01背包的状态,只不过状态转移方程需要改一下f[i][j]=max(f[i-1][j],f[i-1][j-k*c[i]]+k*w[i])。代码的话只需要在j循环里面枚举一下k就行了。代码如下:
for(i=1;i<=n;i++) { for(j=c[i];j<=m;j++) { for(k=1;k<=j/c[i];k++) { f[i][j]=max(f[i-1][j],f[i-1][j-k*c[i]]+k*w[i]); } } }
还记得刚刚讨论01背包时说的那种一维数组的做法吗,当时说的是j只能从m到c[i]进行枚举,不然可能就会将这件物品选择多次。但是这道题每件物品不是可以无限次选取吗,正好。
for(i=1;i<=n;i++) { for(j=c[i];j<=m;j++) { f[j]=max(f[j],f[j-c[i]]+w[i]); } }
当然,背包九讲上还是提供了一种玄学的方法,就是将i这件物品拆分成多件物品,每件物品的费用为2^k*c[i],价值为2^k*w[i],这样我们就可以组合成任意个背包的总和。(当学了倍增后再看这个,真的感觉很玄学)由于本咸鱼实在不想写这个版本的代码,这种玄学的方法就留给读者去实现啦。
3.多重背包
这个相比较完全背包就多了一个每件物品数量的限制,最简单的方法就是在最暴力的完全背包的代码的基础上改成k<=当前物品的件数。代码就不贴了,反正就改了一个地方,本咸鱼懒……
优秀一点的做法便是利用类似倍增的思想来枚举物品的数量(就是上面说的2^k那种方法),代码懒得写,反正应该不会考吧……(一口毒奶)。
然后背包九讲上的O(VN)的算法感觉没怎么看懂,看懂后再更新一下吧。
4.混合背包
混合了上面三种物品的背包,这个我一般就写几个函数,到时候直接按物品调用函数就ok了,状态的话就统一使用最初的那个f[i][j]表示第i件物品,容量为j的背包……的那个状态,虽然时间复杂度可能比较高,但出错的几率可以降低很多,就不用那么痛苦地调代码了。
5.二维费用的背包问题
这个就是每件物品所需要的费用多了一种,其他的条件没有变。
这样的话就把状态加一维,枚举状态的时候多枚举一维就Ok了。状态转移方程式就是这样f[i][j][k]=max(f[i-1][j][k],f[i-1][j-c[i]][k-d[i]]+w[i])。
让我比较在意的是背包九讲中所提到的这种“隐含的第二维费用”:
“有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取 U 件物品。 这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为 1,可以 付出的最大件数费用为 U。换句话说,设 F[v,u] 表示付出费用 v、最多选 u 件时可得 到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后在 f[0...V,0...U] 范围内寻找答案。(我觉得他讲得够明白了,就不做过多解释了)”
对,没有错,就是类似这种看起来根本不像是二维费用的背包,本咸鱼感觉根本想不到啊,算了,写完这篇博客多做题吧。
多吐槽一句,在这种背包的末尾背包九讲的作者写了这样一句话:“当发现由熟悉的动态规划题目变形得来的题目时,在原来的状态中加一维以满足新的限制是一种比较通用的方法。希望你能从本讲中初步体会到这种方法。”我体会不到啊……,怎么办,我退群哦不退役算了……
6.分组背包
这种背包我对他始终没有什么感觉,每次碰见还是不会做,ORZ:
有 N 件物品和一个容量为 V 的背包。第 i 件物品的费用是 Ci,价值是 Wi。这些 物品被划分为 K 组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包 可使这些物品的费用总和不超过背包容量,且价值总和最大。
好吧,因为每组的物品最多选一个,所以我们就可以把它当作01背包来看,f[k][j]表示前k组物品中,装一个背包容量为j的背包的最大价值。f[k][j]=max(f[k-1][j],f[k-1][j-c[i]]+w[i])i ∈group k,久违地来贴一下代码吧。
vector<int>group_k[MAX_K];//记录group k中有哪些物品 for(k=1;k<=K;k++) { for(j=0;j<=m;j++) { int n=group_k[k].size(); for(i=0;i<n;i++) { if(c[i]<=j) f[k][j]=max(f[k-1][j],f[k-1][j-c[i]]+w[i]); } } }
如果f数组要降维的话就按照01背包降维那样降维,并且j从m到0循环就可以了。
7.有依赖的背包问题
既然背包九讲中提到了金明的预算方案那我就拿金明的预算方案来说这个吧。
金明的预算方案可以说是有依赖的背包问题中的裸题了题目在此我也不赘述了,就说说应该怎么做吧。因为附件必须依赖主件,所以当我们要选择附件的时候必须连同主件一起选择,所以我们就可以开一个辅助数组来确定这件主件,以及其所可以携带的附件是否携带,或者是携带哪几个,意思就是在必须选择本主件的情况下,对其附件进行01背包,然后将这个辅助数组的值与主数组比较。可能说得不是很明白,直接上代码应该更直观吧。
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; int v[100],p[100],q[100],n,m,s[200000],t[200000]; void Init() { memset(s,0,sizeof(s)); memset(t,0,sizeof(t)); scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) scanf("%d%d%d",&v[i],&p[i],&q[i]),p[i]*=v[i]; } void work() { int i,j,k; for(i=1;i<=m;i++) { if(q[i]==0) { for(j=1;j<=v[i];j++) t[j]=0; for(j=v[i];j<=n;j++) t[j]=s[j-v[i]]+p[i]; for(j=1;j<=m;j++) { if(q[j]==i) { for(k=n;k>=v[i]+v[j];k--) { t[k]=max(t[k],t[k-v[j]]+p[j]); } } } for(j=v[i];j<=n;j++) { s[j]=max(s[j],t[j]); } } } printf("%d",s[n]); } int main() { Init(); work(); return 0; }
我最初想的状态本来是f[i][j]表示选择到第i个主件的时候,背包容量为j时所获得的最大价值,但后来发现比较难写,并看了题解后才确定了最后的状态。以后这种题还是需要多练(前提是在我不会今年noip后退役)
8.泛化物品
泛化物品简单来说就是物品的价值会随你分配给它的费用而改变,如果是01背包,假设这件物品的价值-费用函数关系为h(x),f[j]表示费用为j的背包能获得的最大价值。那么状态转移方程式也就不难得出f[j]=max(f[j],f[j-x]+h[x]).其他形式的背包也应该差不多吧……
9.背包问题问法的变化
由于本咸鱼学艺不精,加上背包问题的千变万化,这一点我就不写了,怕误导诸位,加上误导我自己。
10、好像有一种背包没有提到
在本咸鱼的的记忆中好像有一种背包要求必须装满。这种背包做法和01背包相似,但是数组赋初值的时候一定要注意,初值用memset应该赋为0x80!!!!!!