背包问题九讲笔记

来源:互联网 发布:好笑的段子 知乎 编辑:程序博客网 时间:2024/06/05 02:12

其实之前已经好几次想读玩崔添翼的《背包九讲》,不过之前总是由于各种缺乏耐心没有啃下来,这次决定要好好读完。

背包九讲来源 :

https://github.com/tianyicui/pack  

1 01 背包问题 

1.1
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。

将前 i 件物品放入容量为 v 的背包中”这个子问题,若只考虑第 i 件物品的策略(放或不放),那么就可以转化为一个只和前 i 1 件物品相关的问题。

如果不放第 i 件物品,那么问题就转化为“前 i 1 件物品放入容量为 v 的背包中”,价值为 F [i 1, v];

如果放第 i 件物品,那么问题就转化为“前 i 1 件物品放入剩下的容量为vCi 的背包中” 

用子问题定义状态:即 F[i, v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:

F[i,v] = max{F[i1,v],F[i1,vCi] +Wi}

F[i][v]把前i件物品塞进重量为V的背包中,如果不放入第i件物品,就是F[i-1][v],如果放入,就是F[i-1][v-Wi]+vi;

体会:对于动态规划问题,先考虑清楚最优子结构是否成立、用哪种状态,然后根据题目写出几种可能导致状态转移的条件就可以了,比如背包问题只有两种:选或者不选。而对于最小编辑记录,三种状态转移方式:删除、增加、修改,也是只有三种情况,一般对于这些情况根据条件取最小或者最大就行了。

1.2 优化空间复杂度 

对于优化空间复杂度:本来的空间复杂度是O(N*W),N是物品数目,W是背包重量

我们注意F[i,v]是由F[i1,v]F[i1, vCi]两个子问题递推而来,能否保证在推F[i, v]时(也即在第i次主循环中推F[v]时)能够取用F[i1,v]F[i1,vCi]的值呢? 事实上,这要求在每次主循环中我们以vV . . .0的递减顺序计算F[v],这样才能保证计算F[v]F[vCi]保存的是状态F[i1,vCi]的值。

如果将 v的循环顺序从上面的逆序改成顺序的话,那么则成了F[i, v]F[i, vCi]推导得到,这是什么意义呢?这样的话可能会导致两次取用同一个物品,因为如果从前往后,那么取用这个物品后,后面的F[i]也会受到前面的影响。而从后往前的性质是后面的操作不会更新前面的状态,而我们确定当前状态只需要前面的状态就好了,这样就不会导致错误。

1.3初始化问题

这个问题通常有两种不同的问法:

(1)恰好装满背包(这其实是硬币找零扩展到完全背包版问题)

(2)重量为V的背包 

这两种情况下初始条件F[0][i]应该是怎么样的呢?

如果是第一种问法,要求恰好装满背包,那么在初始化时除了F[0]0,其它F[1..V]均设为−∞,这样就可以保证最终得到的F[V]是一种恰好装满背包的最优解。

如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将F[0..V]全部设为0

这是为什么呢?可以这样理解:初始化的 F 数组事实上就是在没有任何物品可以放入背包时的合法状态如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装且价值为0的情况下被“恰好装满”,其它容量的背包均没有合法的解,(0件物品使得重量为V的背包被装满的价值最大值)属于未定义的状态,应该被赋值为-∞了。

如果背包并非必须被装满,那么任何容量的背包开始时候(0件物品塞入重量为V的背包的价值最大值)都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。 


2完全背包问题 

2.1题目

N种物品和一个容量为V的背包,每种物品都有无限件可用。放入第i种物品的费用是Ci,价值是Wi。求解:将哪些物品装入背包,可使这些物品的耗费的费用总和不超过背包容量,且价值总和最大。

2.2基本思路

这个问题非常类似于 01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件......直至取V/Ci件等许多种。

如果仍然按照解 01背包时的思路,令F[i,v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:

F[i,v]=max{F[i1,vkCi]+kWi|0kCiv}
这跟 01背包问题一样有O(VN)个状态需要求解,但求解每个状态的时间已经不

是常数了,求解状态 F[i, v]的时间是O(V/Ci ),总的复杂度可以认为是O(N VΣV/Ci),是

比较大的。

01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包

问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是要试图改进这个复杂度。 

2.3一个简单有效的优化

完全背包问题有一个很简单有效的优化,是这样的:若两件物品ij满足CiCjWiWj,则将可以将物品j直接去掉,不用考虑。

这个优化的正确性是显然的:任何情况下都可将价值小费用高的j换成物美价廉的i,得到的方案至少不会更差。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。 

这个优化可以简单的 O(N2)地实现,一般都可以承受。另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V+N)地完成这个优化。这个不太重要的过程就不给出伪代码了,希望你能独立思考写出伪代码或程序。 

2.4转化为01背包问题求解

01背包问题是最基本的背包问题,我们可以考虑把完全背包问题转化为01背包问题来解。

最简单的想法是,考虑到第 i 种物品最多选V/Ci件,于是可以把第i种物品转化为V/Ci件费用及价值均不变的物品,然后求解这个01背包问题。这样的做法完全没有改进时间复杂度,但这种方法也指明了将完全背包问题转化为01背包问题的思路:将一种物品拆成多件只能选0件或1件的01背包中的物品。

更高效的转化方法是:把第 i 种物品拆成费用为Ci2k、价值为Wi2k的若干件物品,其中k取遍满足Ci2kV的非负整数。

这是二进制的思想。因为,不管最优策略选几件第i种物品,其件数写成二进制后,总可以表示成若干个2k件物品的和。这样一来就把每种物品拆成O(logV/Ci)件物品,是一个很大的改进。 

为什么这个算法就可行呢?首先想想为什么 01背包中要按照v递减的次序来循环。让v递减是为了保证第i次循环中的状态F[i,v]是由状态F[i1,vCi]递推而来。

换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果F[i1, vCi]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果F[i, vCi],所以就可以并且必须采用v递增的顺序循环。这就是这个简单的程序为何成立的道理。


值得一提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。 也可以不对重量,对价值来做循环,也可能带来优化

可以用dp[i][j]前i件物品达到价值为j的最小重量。
dp[0][0]=0 dp[0][j]=INF;

3 多重背包问题

3.1题目

N种物品和一个容量为V的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci,价值是Wi。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。 

3.2基本算法

这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可。

因为对于第 i种物品有Mi+1种策略:取0件,取1件......取Mi件。令F[i,v]表示前i种物品恰放入一个容量为v的背包的最大价值,则有状态转移方程:

F [i,v] = max{F[i1, vkCi] +k Wi|0kMi}复杂度是O(VΣMi)。 


3.3转化为01背包问题

另一种好想好写的基本方法是转化为 01背包求解:把第i种物品换成Mi01背包中的物品,则得到了物品数为ΣMi01背包问题。直接求解之,复杂度仍然是O(VΣMi)

但是我们期望将它转化为 01背包问题之后,能够像完全背包一样降低复杂度。

仍然考虑二进制的思想,我们考虑把第 i 种物品换成若干件物品,使得原问题中第i种物品可取的每种策略——取0...Mi件——均能等价于取若干件代换以后的物品。另外,取超过Mi件的策略必不能出现。

方法是:将第 i种物品分成若干件01背包中的物品,其中每件物品有一个系数。这件物品的费用和价值均是原来的费用和价值乘以这个系数。令这些系数分别为1,2,22...2k1,Mi2k+1,且k是满足Mi2k+1>0的最大整数。例如,如果Mi13,则相应的k= 3,这种最多取13件的物品应被分成系数分别为1,2,4,6的四件物品。 

这样就将第 i种物品分成了O(logMi)种物品,将原问题转化为了复杂度为O(VΣlogMi)01背包问题,是很大的改进。

下面给出 O(logM)时间处理一件多重背包中物品的过程:

def MultiplePack(F,C,W,M)if C · M ≥ VCompletePack(F,C,W)returnk←1while k < MZeroOnePack(kC,kW)<span style="font-family: Arial, Helvetica, sans-serif;">//考虑第2^k个</span>
M ←M − kk ← 2kZeroOnePack(C · M,W · M) //考虑余数


对以上代码的理解:如果V[i]*M[i]>V 这件物品对于背包的重量不会造成问题,那么答案就和完全背包一样

否则,把这件物品拆成几个物品,这几个物品有什么特征呢?是这些数能表示从1到M[i]的所有数:

对于每个粘起来的物品,可以看成01,10,100,1000.....的二进制数,对于0到M-R( M2+1 的最大整数)间的所有数字,这些数字都能被表示,而对于1到R的所有数字,也都能用这些粘起来的数字表示,所以对于取0个到M个,都能用这些粘起来的物品来表示。

为什么能够全部表示就是正确的呢?因为动态规划中只有一种可能在当前情况是最优的,而这种可能在0个到M个之间,如果这些粘起来的物品能正确表示0个到M个物品之间的所有物品,就能搜索到这种正确情况。

3.4可行性问题O(VN)的算法

当问题是“每种有若干件的物品能否填满给定容量的背包”,只须考虑填满背包的可行性,不需考虑每件物品的价值时,多重背包问题同样有O(V N) 复杂度的算法。 

例如,可以使用单调队列的数据结构,优化基本算法的状态转移方程,使每个状态的值可以以均摊O(1)的时间求解。




0 0
原创粉丝点击