动态规划专题(二):解决0-1背包问题并找到所有最优方案

  小明有一个容量为w的背包,现在有n件物品,给定这些物品的价值与重量,求小明应如何选择物品使背包中的物品价值最大。
  本文解法并不能严格称为动态规划,只是使用了动态规划的思想对递归进行了优化。将每次递归所得结果记录下来,下次达到相同的状态时直接查询即可,不需再次计算。实际上,这种思想可以相对显著的优化递归的性能。

获取最大值

  我在做这个题目的时候走了一些弯路,这里我把我的错误代码也放出来,随时提醒自己,也希望大家以后避免犯类似的错误。
  以下是错误代码:

class Knapsack(object):
    def __init__(self, weight_list, value_list, max_weight):
        self.weight_list = weight_list.copy()
        self.value_list = value_list.copy()
        self.item_num = len(weight_list)
        self.max_weight = max_weight
        self.map = [{} for _ in range(self.item_num)]
        self.path = []
        self.solution = []
        self.dynamic_programming(0, 0, 0)
        self.max_value = self.map[0][0]
        self.get_path()

    def dynamic_programming(self, index, current_weight, current_value):
        if self.item_num == 0:
            return 0
        item_weight = self.weight_list[index]
        item_value = self.value_list[index]
        if current_weight in self.map[index]:
            return self.map[index][current_weight]
        elif index == self.item_num - 1:
            if item_weight + current_weight <= self.max_weight:
                self.map[index][current_weight] = current_value + item_value
                return self.map[index][current_weight]
            else:
                self.map[index][current_weight] = current_value
                return current_value
        else:
            if item_weight + current_weight <= self.max_weight:
                select_value = self.dynamic_programming(index + 1, current_weight + item_weight, current_value + item_value)
                not_select_value = self.dynamic_programming(index + 1, current_weight, current_value)

                max_value = max(select_value, not_select_value)
                self.map[index][current_weight] = max_value
                return max_value
            else:
                self.map[index][current_weight] = self.dynamic_programming(index + 1, current_weight, current_value)
                return self.map[index][current_weight]

    def get_path(self):
    	pass

  如果你有兴趣,可以想一想以上代码错在哪里。如果你是高手,说不定一眼就能看出来我这只小菜鸟错哪了。以下是正确代码:

class Knapsack(object):
    def __init__(self, weight_list, value_list, max_weight):
        self.weight_list = weight_list.copy()
        self.value_list = value_list.copy()
        self.item_num = len(weight_list)
        self.max_weight = max_weight
        self.map = [{} for _ in range(self.item_num)]
        self.path = []
        self.solution = []
        self.dynamic_programming(0, 0)
        self.max_value = self.map[0][0]
        self.get_path()

    def dynamic_programming(self, index, current_weight):
        if self.item_num == 0:
            return 0
        item_weight = self.weight_list[index]
        item_value = self.value_list[index]
        if current_weight in self.map[index]:
            return self.map[index][current_weight]
        elif index == self.item_num - 1:
            if item_weight + current_weight <= self.max_weight:
                self.map[index][current_weight] = item_value
                return item_value
            else:
                self.map[index][current_weight] = 0
                return 0
        else:
            if item_weight + current_weight <= self.max_weight:
                select_value = item_value + self.dynamic_programming(index + 1, current_weight + item_weight)
                not_select_value = self.dynamic_programming(index + 1, current_weight)

                max_value = max(select_value, not_select_value)
                self.map[index][current_weight] = max_value
                return max_value
            else:
                self.map[index][current_weight] = self.dynamic_programming(index + 1, current_weight)
                return self.map[index][current_weight]
                
	def get_path(self):
    	pass

  如果你刚才没看出来第一段代码错在哪里,看了正确的代码,或许你会有一些新想法,可以想想两段代码之间的本质区别在哪里。
  两端代码其实思路相似,原理也很简单:每次递归有选取与不选取物品index两个选项,在满足约束条件的情况下,对所有可能情况进行递归。
  两段代码的表面区别其实不大,好像只是状态值的选取稍不同而已,第一段代码以物品0到物品index的选取物品价值之和为状态的值,第二段代码以物品index到物品n的选取价值之和为状态的值。但实际上,物品0到物品index的选取价值之和并不一定是当前状态的最优解。
  原因:在前index个物品中,若存在至少两种选取物品的方法记为方法1方法2,满足方法1方法2的选取结果的重量相同但方法1的选取价值之和不大于方法2的选取价值之和,且方法1会先被递归并记录,则在方法2递归到物品index时,由于该状态已在递归方法1时被记录,此时方法2会直接查询之前的记录。但显然,方法2中的物品index之后的物品在被选取时,由于可用的重量更大,因此之后的选取物品价值之和一定不小于方法1。故第一段代码错误。
  实际上,若使用单纯的递归,即到达以前到达过的状态时不做查询操作,而继续递归,两段代码都可以得到正确的最大值。因为若不做查询操作,两段代码都会对所有的可能选取方法进行完整的遍历。

一点心得体会

  我想在这里记录一下我的心得体会。递归、动态规划之类的题目(以下简称动态规划)做多了之后我个人觉得,在动态规划的过程中若想获取所有可能的最优方案其实是一个比较令人头秃的事情。
  首先,程序本身可能并不需要在到达每个状态时都更新一下全局最优解。所以除非进行额外的更新操作,否则只能记录所有可能方案,会显著降低程序效率,而且有时候分类讨论的情况较多,若在每一种情况中都进行更新操作会让程序显得极其笨重。
  其次,或许记录一个最优解并不难,但记录所有最优解绝不容易。以此题为例,当选取物品index和不选取物品index时之后的选取物品价值之和相同时怎么办?一定要办可能也能办,但肯定令人头秃。基本上所有的动态规划问题在更新状态时都会遇到类似的问题。
  再者,在过程中记录面临的另一个问题就是我们不知道全局最优解是什么,所以对所有可能的全局最优解对应的方案都要进行一系列的记录操作,效率低下。
  综上所述,在递归或动态规划过程中去获取最优方案或许并不是一个好方法。此时,我们需要改变思路。在动态规划过程中记录下获取潜在最优方案的必要条件即可,动态规划结束后,根据已知的全局最优解与所有潜在最优方案的必要条件进行倒推,从而得到所有最优方案。在已知全局最优解的情况下去寻找所有最优方案要容易得多,就算获取的方式复杂一些,由于最优方案数量及其有限,所以计算量相对很小,更重要的是可以少掉很多头发。

获取所有最优方案

  在动态规划的过程中,我们已经在self.map中记录下了所有潜在最优方案中的状态,我们要做的就是根据最优解,倒推出最优方案。
  思路:self.map[0][0]一定是最优解,若选取物品0,则在self.map[1]中一定存在键值对self.map[1][self.max_value - value_list[0]];若选取不物品0,则在self.map[1]中一定存在键值对self.map[1][self.max_value]。若两种情况都有存在对应的键值对,则说明取或者不取都可以,此时需要分别进行遍历。具体的过程类似于二叉树的深度优先遍历,有兴趣的朋友可以移步我的另一篇文章,这里不再赘述。

    def get_path(self, index=0, current_weight=0, current_value_left=None):
        if index == 0:
            current_value_left = self.max_value
        if index == self.item_num - 1:
            if current_weight + self.weight_list[index] <= self.max_weight:
                self.path.append(index)
                self.solution.append(self.path.copy())
                self.path.remove(index)
            else:
                self.solution.append(self.path.copy())
        else:
            select_weight = current_weight + self.weight_list[index]
            select_value_left = current_value_left - self.value_list[index]
            if select_weight <= self.max_weight:
                if self.map[index + 1][select_weight] == select_value_left and \
                        self.map[index + 1][current_weight] == current_value_left:
                    self.path.append(index)
                    self.get_path(index + 1, select_weight, select_value_left)
                    self.path.remove(index)
                    self.get_path(index + 1, current_weight, current_value_left)
                elif self.map[index + 1][select_weight] == select_value_left:
                    self.path.append(index)
                    self.get_path(index + 1, select_weight, select_value_left)
                    self.path.remove(index)
                else:
                    self.get_path(index + 1, current_weight, current_value_left)
            else:
                self.get_path(index + 1, current_weight, current_value_left)

  为了方便大家更好的学习,附上本题的算例。算例在手,天下我有!算例
  若有任何指正之处或交流心得体会请在评论区留言。若喜欢这篇文章记得点个赞哦!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值