动态规划算法-03背包问题

背包问题

  • 简述
    • 一个很著名的动态规划问题,也称为0-1背包问题。
  • 问题描述
    • 有N个重量为w1,w2,w3,,,wn、价值为v1,v2,v3,,,vn的物品和一个承重量为W的背包,求让背包里装入的物品具有最大的价值总和的物品子集。
    • 看到这种最优解求解的题目,很容易想到动态规划思路。
  • 问题分析
    • 总体思路
      • 为了设计动态规划算法,需要推导出一个递推关系以构造递归函数,用较小子问题的解的形式来表示背包问题的当前问题的解。
    • 首先考虑一个有前i(1<=i<=N)个物品定义的实例,物品的重量分别为w1,w2,,,wi,价值为v1,v2,,,vi,背包目前的承重量为j(1<=j<=W)。
    • 设F(i,j)为组成该实例最优解的物品的总价值,也就是说能够放进承重量为j的背包中的前i个物品中最有价值的子集的总价值。(注意,在这里还没有出现题目想要的解的形式,)
    • 在这里,可以把前i个物品中能够放进承重量为j的背包中的子集分为两种类别:包括第i个物品的子集和不包括第i个物品的子集。于是可以得到以下结论:
      • 根据定义,在不包括第i个物品的子集中,最优子集的价值是F(i-1,j)。
      • 在包括第i个物品的子集中(j-wi>=0),最优子集为该物品和前i-1个物品中能够放进承重量为j-wi的背包的最优子集组成。这种最优子集的总价值等于vi+F(i-1,j-wi)。
    • 因此,在前i个物品中,最优解的总价值等于以上两种情况的最大值,这就是最优子结构了。
    • 当然,如果第i个物品不能放进背包中,那么从前i个物品中选出的最优子集的总价值即等于从前i-1个物品中选出的最优子集的总价值。于是得到下面的递推式,也是状态转移函数
      • F(i,j)=max{F(i-1,j),vi+F(i-1,j-wi)},j-wi>=0
      • F(i,j)=F(i-1,j),j-wi<0
    • 同时可以得到边界
      • F(0,j)=0,j>=0
      • F(i,0)=0,i>=0
        -至此,动态规划的三要素寻找完成,我们的目标不仅仅是求F的值,还有F取值时的物品组合。
  • 实例分析
    • 假设物品数量为5,背包承重量为10,各个物品信息如下表。

      编号 重量w 价值v
      2 6
      2 3
      6 5
      5 4
      4 6
    • 动态规划的原理与分治法常常类似,但是分治法将子问题和子子问题反复求解多次,动态规划擅长的则是记录之前问题的解,即填表。

      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
      [0, 0, 6, 6, 6, 6, 6, 0, 6, 0, 6]
      [0, 0, 0, 0, 9, 9, 9, 0, 0, 0, 9]
      [0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 14]
      [0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 14]
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15]
    • 可以看到,当输入为5,10的时候,最优解为15,这是正确的。

  • 回溯求序列
    • 一般,动态规划用来求最优解,利用递归构造可以很方便的找到最后的答案。
    • 注意:一旦,动态规划的题目出现输入序列,那么就不是找到答案那么简单,除了正向找到最优解,构造解表,还要反向寻找最优解的构成。
      • 以上面这个例子为例,最后rst[5,10]是15,这是答案,此时定义一个物品长度的序列x并使每个元素为False。
      • 对表格的每一行做遍历,指定w为最高权重10.逆序遍历表格每一行,最后一行根据函数判断rst[m][w](m为遍历下标)是否等于rst[m-1][w]若相等,则代表当前行对应的物品没有选中(原理是依据状态转移函数),否则代表选中,w-=当前行对应物品的权重,x[m]置为True,按此顺序,遍历结束。
      • 输出为True的对应下标,即为最优解序列。
      • 在这个过程中x的取值要么为True要么为False,这就是为什么叫做0-1背包问题。
  • 代码
    •   def bag(i, j, w, v, out):
            if i == 0 and j >= 0:
                out[i][j] = 0
                return 0
            if i >= 0 and j == 0:
                out[i][j] = 0
                return 0
            if j - w[i-1] >= 0:
                out[i][j] = max(bag(i-1, j, w, v, out), v[i-1] + bag(i-1, j-w[i-1], w, v, out))
                return out[i][j]
            if j - w[i-1] < 0:
                out[i][j] = bag(i-1, j, w, v, out)
                return out[i][j]
        
        
        def search(rst, i, j):
            x = [False for m in range(i+1)]
            for m in range(i, -1, -1):
                if rst[m][j] == rst[m-1][j]:
                    # 此时代表没有选中当前
                    pass
                else:
                    x[m] = True
                    j -= w[m-1]
            for i in range(len(x)):
                if x[i]:
                    print("选择了第{}个物品".format(i))
        
        
        if __name__ == '__main__':
            # 物品数目
            i = 5
            # 背包容量
            j = 10
            # 物品信息
            w = [2, 2, 6, 5, 4]
            v = [6, 3, 5, 4, 6]
            # 结果存放表
            rst = [[0 for m in range(j+1)] for n in range(i+1)]
            # 计算结果,构造解集合
            bag(5, 10, w, v, out=rst)
            # 输出解集合表格
            for item in rst:
                print(item)
            # 回溯查找输入序列
            search(rst, i, j)
      
  • 补充说明
    • 具体代码可以查看我的Github,欢迎Star或者Fork
    • 参考书《你也能看得懂的Python算法书》,书中略微有一点不合理之处,做了修改
    • 到这里,其实你已经体会到了动态规划的简约之美,当然,要注意Python是有递归深度限制的,如不是必要,建议使用循环控制

你可能感兴趣的:(Python,算法)