背包问题汇总

本文主要来源于《背包问题九讲》,我主要选择了比较简单的0-1背包问题和完全背包问题进行汇总,并加入了python代码实现,刚重装系统,手头没有C编译器,汗。


一、背包问题概述

        背包问题包括0-1背包问题、完全背包问题、部分背包问题等多种变种。其中,最简单的是部分背包问题,它可以采用贪心法来解决,而其他几种背包问题往往需要动态规划来求解。本文对几种背包问题进行总结,同时给出实现代码,如有错误,请各位大虾指正。


二、部分背包问题

         部分背包问题描述:有N件物品和一个容量为C的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使价值总和最大。 注意这里不要求把物品整个装入,可以只装入一个物品的部分。
         部分背包问题常采用贪心算法来解决,先对每件物品计算其每单位重量价值v[i]/w[i],然后从具有最大单位价值的物品开始拿,然后拿第二大价值的物品,直到装满背包。按照这种贪心策略拿到的必然是价值总和最大。由于该实现代码简单,所以此处略去。

三、0-1背包问题

问题描述

        0-1背包问题描述:有N件物品和一个容量为C的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使价值总和最大。 注意物品只能要么拿要么不拿,这也正是0-1的意义所在。可以把部分背包问题看作是拿金粉,而0-1背包问题则是拿金块,一个可分,一个不可分。

基本思路

        这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。 用子问题定义状态:即f[i][w]表示前i件物品恰放入一个容量为c的背包可以获得的最大价值。则其状态转移方程便是: 
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]。

优化空间复杂度

        以上方法的时间和空间复杂度均为O(CN),其中时间复杂度应该已经不能再优化了,但空间复杂度却可以优化到O(N)。 由于在计算f[i][c]的时候,我们只需要用到f[i-1][c]和f[i-1][c-w[i]],所以完全可以通过一维数组保存它们的值,这里用到的小技巧就是需要从c=C...0开始反推,这样就能保证在求f[c]的时候f[c-w[i]]保存的是f[i-1][c-w[i]]的值。 注意,这里不能从c=0...C这样顺推,因为这样会导致f[c-w[i]]的值是f[i][c-w[i]]而不是f[i-1][c-w[i]。这里可以优化下界,其实只需要从c=C...w[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了。


四、完全背包问题

问题描述

    有N种物品和一个容量为C的背包,每种物品都有无限件可用。第i种物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

基本思路

    这个问题非常类似于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背包问题输出,下半段为完全背包问题输出):

背包问题汇总_第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)解法

我们可以采用与0-1背包问题相反的顺序遍历,从而可以得到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])
    




你可能感兴趣的:(背包问题汇总)