0-1背包问题的Python实现及其优化

1. 概述

有一个背包,它的容量为C (Capacity)。现在有n种不同的物品,编号为0…n-1,其中每一件物品的重量为w(i),价值为v(i)。 问可以向这个背包中盛放哪些物品,使得在不超过背包容量的基础上,物品的总价值最大。

暴力解法: 每一件物品都可以放进背包,也可以不放进背包。时间复杂度为:O((2^n)*n)

另辟蹊径: F(n, C)考虑将n个物品放进容量为C的背包,使得价值最大。一件物品只有选与不选两种情况,也即每件物品只能选一次。
(1)当不考虑选取第 i 件物品时,F(i, C) = F(i - 1, C);
(2)当考虑选取第 i 件物品时,F(i, C) = v(i) + F(i - 1, C - w(i))
要在上述两种情况中取最大值,所以状态转移方程即为:

F(i, C) = max( F(i - 1, C), v(i) + F(i - 1, C - w(i)) )

2. 第一种方法——记忆化搜索

(1)主函数中传入w数组(每个物品的容量),v数组(每个物品的价值)和C(背包的总容量)
(2)使用一个递归函数bestValue,在这个函数中首先传入w和v,index表示考虑到的物品的序列号,以及要处理的背包容量C (bestValue的含义就是用[0…index]的物品,填充容积为C的背包的最大价值)

  • 首先看递归的终止条件:若当前的index<0,也就是说此时物品的集合是一个空集了,选不出任何东西了 或者 当前的背包容量c<=0,也就是说无法装入任何新的物品了,此时就应该返回0 (简单点说就是无法选物品或无法装物品)
  • 否则就要尝试向背包中放入新的物品:考虑物品集合中的最后一个物品index该不该放进去
    (1)一是这个物品根本不去管——bestValue(w, v, index-1, c)
    (2)二是将这个index物品放进背包里,为了保险起见,此处需先确定当前背包的容量大于等于index物品的容积(if c>= w[index]),这时的价值就是v[index] + bestValue(w, v, index-1, c - w[index])
    要将两者取最大值给res,返回res即可。

但是又存在重叠子问题来源于index和c构成了一个数据对,求解过程中同样的数据对可能出现多次,因此可以用记忆化搜索。由于背包问题有两个约束条件(w和v),每个状态是被两个变量所定义的,所以开辟的记忆化空间应该是一个二维数组 memo=[[-1 for j in range(C+1)] for i in range(n)] (一共有n行,C+1列(来表示从0到C这么多的容量))

  • 递归终止条件依然不变;
  • 否则就要看 memo[index][c] 这个状态之前有没有被计算过,若被计算过,直接返回 memo[index][c] 即可;
  • 否则才运行下面的逻辑,计算出来res之后,还要将res的值记录下来(memo[index][c]=res),最后返回res即可。
class Knapsack01:
    def knapsack01(self, w, v, C):
        n = len(w)
        self.memo = [[-1 for j in range(C + 1)] for i in range(n)]
        return self.bestValue(w, v, n-1, C)

    def bestValue(self, w, v, index, c):
        if index < 0 or c <= 0:
            return 0
        if self.memo[index][c] != -1:
            return self.memo[index][c]
        res = self.bestValue(w, v, index-1, c)
        if c >= w[index]:
            res = max(res, v[index] + self.bestValue(w, v, index - 1, c - w[index]))
        self.memo[index][c] = res
        return res

k = Knapsack01()
w = [1, 2, 3]
v = [6, 10, 12]
C = 5
print(k.knapsack01(w, v, C))
输出:22

3. 第二种方法——动态规划

使用动态规划,依旧是先确定n的数值(n=len(w)),注意: 我们传入了两个数组w和v,那么这两个数组所含有的元素个数应该是一样的:assert(len(w) == len(v)),有了n之后就可以设立动态规划的二维数组,有n行,每一行有C+1列(从0到C)memo=[[-1 for j in range(C+1)] for i in range(n)]

  • 动态规划是自底向上的,所以我们先处理最基础的问题:先看memo第0行的每个元素是多少,进行一个for循环遍历列:for j in range(0, C+1)memo[0][j]里的j表示此时处理的背包容量,只考虑0号物体 ,所以将j的大小和w[0]进行比较,若j>=w[0],就能将0号物品放进去,最大值就是v[0];否则的话背包中无法放入物品,最大值就是0,为了保险起见,需要添加上若n=0(也即传进来的w和v中没有任何元素),直接返回0即可。
  • 最基础的问题解决了,剩下的问题就是一个递推的过程,现在是逐行的解决问题for i in range(1, n+1)因为基础问题已经处理过第0行,所以此处从第1行开始),在每行中逐列的解决问题for j in range(0, C+1),在每一列中要处理的是计算memo[i][j](考虑0~i这些物品且容积为j的背包获得的最大值)的值:
    (1)第一种情况是不考虑 i,直接memo[i][j] = memo[i-1][j]
    (2)第二种情况是考虑 i,但需要判断此时的容量 j 是否大于等于w[i](即第 i 个物品确实能放进背包中)——memo[i][j] = max(memo[i][j], v[i] + memo[i-1][j-v[i]])
    整个循环一直进行下去,直到求出memo[n-1][C],直接返回该值即可。

使用for x in memo可以很清晰的查看 memo 这个二维数组中的每个数字,右下角的最后一个数即为所求。

class Knapsack01:
    def knapsack01(self, w, v, C):
        assert (len(w) == len(v))
        n = len(w)
        if n == 0:
            return 0
        memo = [[-1 for j in range(C + 1)] for i in range(n)]
        for j in range(0, C + 1):
            memo[0][j] = v[0] if j >= w[0] else 0
        for i in range(1, n):
            for j in range(0, C + 1):
                memo[i][j] = memo[i - 1][j]
                if j >= w[i]:
                    memo[i][j] = max(memo[i][j], v[i] + memo[i - 1][j - w[i]])
        for x in memo:
            print(x)
        return memo[n - 1][C]

k = Knapsack01()
w = [1, 2, 3]
v = [6, 10, 12]
C = 5
print(k.knapsack01(w, v, C))
输出:
[0, 6, 6, 6, 6, 6]
[0, 6, 10, 16, 16, 16]
[0, 6, 10, 16, 18, 22]
22

4. 优化算法空间(只使用两行)

上述动态规划的时间复杂度为O(n * C),空间复杂度为O(n * C)。但实际上第 i 行元素只依赖于第 i-1 行元素,理论上,只需要保持两行元素即可,不需要保持n行元素,这样可以优化空间,空间复杂度为O(2 * C)。
在这里插入图片描述
开辟两行空间,最开始第一行是 i=0,第二行处理 i=1 的情况,依靠第一行 i=0 的数据,处理完毕后接着处理 i=2 的情况(i=2 这一行的元素是依靠 i=1这行的元素,与 i=0 这行无关)所以可以将 i=2 覆盖掉 i=0这行……以此类推,可以发现,第一行永远处理的是 i 为偶数时的情况,第二行永远处理的是 i 为奇数时的情况。

具体实现如下:

  • 首先一开始定义memo这个二维数组时,改成两行即可 memo = [[-1 for j in range(C+1)] for i in range(2)]
  • 接下来初始化i=0的这一行不需要变动,重点是下面的循环,我们还是从i=1开始一直到n依次来更新每一行,在更新的过程中,对于第i行来说,实际上使用的是memo这个二维空间中的 i%2 那一行(不是第0行就是第1行,具体是哪一行让它和2求余数,i为偶数时在第0行,为奇数时在第1行),所以就有了 memo[i%2][j] = memo[(i-1)%2][j]表示第i行还是要根据第i-1行来,但是我们的物理空间中只有两行,那么第i行现在在 i%2 这个位置,第i-1行在(i-1)%2这个位置,下面以此类推,一旦涉及到memo的行号,都要将它%2;
  • 最后返回的是memo[(n-1)%2][C],我们依然是要将第n-1行的第C个元素返回回去,只不过当前的物理存储中,第n-1行是在 (n-1)%2 这个位置。
class Knapsack01:
    def knapsack01(self, w, v, C):
        assert (len(w) == len(v) and C >= 0)
        n = len(w)
        if n == 0 or C == 0:
            return 0
        memo = [[-1 for j in range(C + 1)] for i in range(2)]
        for j in range(0, C + 1):
            memo[0][j] = v[0] if j >= w[0] else 0
        for i in range(1, n):
            for j in range(0, C + 1):
                memo[i % 2][j] = memo[(i - 1) % 2][j]
                if j >= w[i]:
                    memo[i % 2][j] = max(memo[i % 2][j], v[i] + memo[(i - 1) % 2][j - w[i]])
        return memo[(n - 1) % 2][C]

k = Knapsack01()
w = [1, 2, 3]
v = [6, 10, 12]
C = 5
print(k.knapsack01(w, v, C))
输出:22

注意:
这个优化使得我们可以处理的背包容量比原先大很多,举例子:假设我们有100个物品,背包容量为100,此时的空间就达到了极限,原来使用n*C的空间复杂度,我们的空间最多有100 * 100=10000个单元,但使用这个优化后,我们可以将这10000个空间重新分配 10000=2 * 5000,换句话说,我们依然可以处理有100个物品的背包问题,但是此时我们处理的背包容量的最大限额达到了5000。

5. 进一步优化算法空间(只使用一行)

如何只使用一行大小为C的数组完成动态规划?
在这里插入图片描述
先看两行的,对于上一行,我们永远只使用正上方和左边的元素,右边的不会去碰,就提示了我们,如果只有一行,在之前的逻辑中每一次更新只参考上面和左边的内容,所以我们可以从右往左的刷新这一行的内容。比如:

下图初始化的是 i = 0 这行的内容
在这里插入图片描述
我们要更新 i = 1 这行的内容,就从最后一列开始,背包容量为5时,可以放下编号为1的物品(重量为2,价值为10),这时就有两种情况:(1)不放编号为1的物品时,直接把上一行该列的数字拿过来,也就是此时的价值为6;(2)放入编号为1的物品时,就是v[i] + memo[i - 1][j - w[i]],也就是将编号为1的物品价值10 加上 上一行(第0行)第3列的数值6 等于16。取两者之间的最大值16放入最后一个数字的位置…依次类推不断向前更新数值,直到当前的容量无法再装下当前的物品。
在这里插入图片描述
具体实现如下:

  • memo就初始化为一维数组:memo = [-1 for i in range(C + 1)]
  • 下面看初始化的过程,首先对第一行的内容进行初始化(也就是i=0时的值进行初始化),因为是一维数组,直接写成memo[j] = v[0] if j >=w[0] else 0
  • 紧接着我们对i=1开始一直到n依次求解,在求解的过程中我们对列的处理,每次都是从C开始的,一直向前处理,直到j大于等于当前要处理的物品的重量for j in range(C, w[i] - 1, -1),因为当 j 比 w[i] 小时,说明当前物品已经不能放入背包里了,那么当前行中停留的那些 j<w[i] 的元素就是上一行中相应的元素:memo[j] = max(memo[j], v[i] + memo[j - w[i]])
  • 最后memo[C]中存放的就是最终的答案。
class Knapsack01:
    def knapsack01(self, w, v, C):
        assert (len(w) == len(v) and C >= 0)
        n = len(w)
        if n == 0 or C == 0:
            return 0
        memo = [-1 for i in range(C + 1)]
        for j in range(0, C + 1):
            memo[j] = v[0] if j >= w[0] else 0
        for i in range(1, n):
            for j in range(C, w[i] - 1, -1):
                memo[j] = max(memo[j], v[i] + memo[j - w[i]])
        return memo[C]

k = Knapsack01()
w = [1, 2, 3]
v = [6, 10, 12]
C = 5
print(k.knapsack01(w, v, C))
输出:22
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值