本文主要来源于《背包问题九讲》,我主要选择了比较简单的0-1背包问题和完全背包问题进行汇总,并加入了python代码实现,刚重装系统,手头没有C编译器,汗。
一、背包问题概述
背包问题包括0-1背包问题、完全背包问题、部分背包问题等多种变种。其中,最简单的是部分背包问题,它可以采用贪心法来解决,而其他几种背包问题往往需要动态规划来求解。本文对几种背包问题进行总结,同时给出实现代码,如有错误,请各位大虾指正。
二、部分背包问题
三、0-1背包问题
问题描述
基本思路
f[i][c]=max{f[i-1][c],f[i-1][c-w[i]]+v[i]}
这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为c的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为f[i-1][c];
如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为c-w[i]的背包中”,此时能获得的最大价值就是f[i-1][c-w[i]]再加上通过放入第i件物品获得的价值v[i]。
优化空间复杂度
for i=0..N-1
for c=C..w[i]
f[c]=max{f[c],f[c-w[i]]+v[i]};
算法实现的python代码如下:
def knap01(N, C): //num为物品数目,C为背包容量,w[i]为物品i的重量,v[i]为物品i的价值,f为结果列表
for i in range(0, N):
for c in range(C, w[i]-1, -1):
f[c] = max(f[c], f[c-w[i]] + v[i])
可以使用下面的完整代码进行测试:
w = [3, 4, 5] //物品重量列表
v = [4, 5, 6] //物品价值列表
f = []
def init(C): //初始化保存结果的列表f
del f[:]
for i in range(0, C+1):
f.append(0)
print 0, f
def knap01(N, C): //0-1背包主函数
for i in range(0, N):
for c in range(C, w[i]-1, -1):
f[c] = max(f[c], f[c-w[i]] + v[i])
print i+1, f
if __name__ == "__main__":
num, totalCapacity = 3, 10 //分别指定物品数目和背包容量
init(totalCapacity)
knap01(num, totalCapacity)
测试结果如下:
初始化的细节问题
我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..C]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..C]全部设为0。
为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
四、完全背包问题
问题描述
基本思路
这个问题非常类似于0-1背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][c]表示前i种物品恰放入一个容量为c的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:
f[i][c]=max{f[i-1][c-k*w[i]]+k*w[i]| 0<=k*w[i]<=c }
这跟0-1背包问题一样有O(CN)个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态f[i][c]的时间是O(c/w[i]),总的复杂度可以认为是O(CN*Σ(c/w[i])),是比较大的。实现代码如下:
#####完全背包问题 解法1########
def knap_complete(N, C):
for i in range(0, N):
for c in range(C, -1, -1):
for k in range(0, c/w[i]+1):
if k*w[i] <= c:
f[c] = max(f[c], f[c-k*w[i]] + k*v[i])
使用与0-1背包问题相同的例子,允许程序结果如下(上半段为0-1背包问题输出,下半段为完全背包问题输出):
转换为0-1背包问题
既然01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第i种物品最多选C/w[i]件,于是可以把第i种物品转化为C/w[i]件费用及价值均不变的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。
更高效的转化方法是:把第i种物品拆成重量为w[i]*2^k、价值为w[i]*2^k的若干件物品,其中k满足w[i]*2^k<=C
。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。这样把每种物品拆成O(log C/w[i])件物品,是一个很大的改进。但我们有更优的O(CN)的算法。
进一步优化—O(CN)解法
for i=0..N-1
for c=w[i]..C
f[c]=max{f[c],f[c-w[i]]+v[i]};
这个伪代码与0-1背包伪代码只是c的循环次序不同而已。0-1背包之所以要按照v=V..0的逆序来循环。这是因为要保证第i次循环中的状态f[i][c]是由状态f[i-1][c-w[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][c-w[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][c-w[i]],所以就可以并且必须采用c=w[i]..C的顺序循环。这就是这个简单的程序为何成立的道理。python实现代码如下:
#####完全背包问题 解法2########
def knap_complete_2(N, C):
for i in range(0, N):
for c in range(w[i], C+1):
f[c] = max(f[c], f[c-w[i]] + v[i])