【问题标题】:Using dynamic programming to solve a version of the knapsack problem使用动态规划解决背包问题的一个版本
【发布时间】:2020-05-06 18:44:52
【问题描述】:

我正在通过 OpenCourseWare (https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0002-introduction-to-computational-thinking-and-data-science-fall-2016/assignments/) 上的 MIT6.0002 工作,但我被问题集 1 的 B 部分难住了。这个问题是作为背包问题的一个版本提出的,说明如下:

[Aucks 发现了一群能下不同重量金蛋的鹅] 他们希望在旅途中尽量少带鸡蛋,因为他们没有太多空间 在他们的船上。他们详细记录了鹅可以产下的所有鸡蛋的重量 在给定的羊群中,他们的船可以承载多少重量。 实现一个动态规划算法来找到最少需要的鸡蛋数量 在 dp_make_weight 中为某艘船设定一个给定的重量。结果应该是一个整数 表示从给定的鹅群中生产所需的最小鸡蛋数量 给定重量。您的算法不需要返回鸡蛋的重量,只需返回 最少的鸡蛋数量。 假设: - 不同鹅之间的所有鸡蛋重量都是唯一的,但给定的鹅总是会产下相同大小的鸡蛋 - Aucks 可以等待鹅产下所需数量的蛋[即每种尺寸的蛋都有无限供应]。 - 总有可用的 1 号鸡蛋

问题还表明解决方案必须使用动态规划。我已经编写了一个解决方案(在 Python 中),我认为它找到了最佳解决方案,但它不使用动态编程,而且我无法理解动态编程是如何适用的。也有人建议解决方案应该使用递归。

谁能向我解释在这种情况下使用记忆化的好处是什么,以及通过实施递归解决方案我会获得什么? (如果我的问题太含糊,或者解决方案对于文字来说太明显了,我深表歉意;我是编程和这个网站的相对初学者)。

我的代码:

#================================
# Part B: Golden Eggs
#================================

# Problem 1
def dp_make_weight(egg_weights, target_weight, memo = {}):
    """
    Find number of eggs to bring back, using the smallest number of eggs. Assumes there is
    an infinite supply of eggs of each weight, and there is always a egg of value 1.

    Parameters:
    egg_weights - tuple of integers, available egg weights sorted from smallest to largest value (1 = d1 < d2 < ... < dk)
    target_weight - int, amount of weight we want to find eggs to fit
    memo - dictionary, OPTIONAL parameter for memoization (you may not need to use this parameter depending on your implementation)

    Returns: int, smallest number of eggs needed to make target weight
    """
    egg_weights = sorted(egg_weights, reverse=True) 
    eggs = 0
    while target_weight != 0:
        while egg_weights[0] <= target_weight:
            target_weight -= egg_weights[0]
            eggs += 1
        del egg_weights[0]
    return eggs


# EXAMPLE TESTING CODE, feel free to add more if you'd like
if __name__ == '__main__':
    egg_weights = (1, 5, 10, 25)
    n = 99
    print("Egg weights = (1, 5, 10, 25)")
    print("n = 99")
    print("Expected ouput: 9 (3 * 25 + 2 * 10 + 4 * 1 = 99)")
    print("Actual output:", dp_make_weight(egg_weights, n))
    print()

【问题讨论】:

    标签: python recursion dynamic-programming


    【解决方案1】:

    这里的问题是典型的 DP 情况,贪婪有时可以给出最优解,但有时不能。

    这个问题的情况类似于经典的 DP 问题coin change,我们希望在给定目标值的情况下找到最少数量的不同价值硬币来找零。在美国等一些国家(使用价值 1、5、10、25、50、100 的硬币)可用的面额是这样的,最好是贪婪地选择最大的硬币,直到价值低于它,然后继续下一个硬币。但是对于其他面额集合,如 1、3、4,反复贪婪地选择最大值会产生次优结果。

    同样,您的解决方案适用于某些蛋重,但不适用于其他蛋重。如果我们选择鸡蛋的权重为 1、6、9,并给出目标权重 14,算法会立即选择 9,然后无法在 6 上取得进展。此时,它会吞下一堆 1,最终认为是 6是最小的解决方案。但这显然是错误的:如果我们明智地忽略 9,先选择两个 6,那么我们可以只用 4 个鸡蛋达到所需的重量。

    这表明我们必须考虑这样一个事实,即在任何决策点,采用我们的任何教派都可能最终导致我们获得全局最优解决方案。但我们暂时无法知道。所以,我们每一步都尝试每一个面额。这非常有利于递归,可以这样写:

    def dp_make_weight(egg_weights, target_weight):
        least_taken = float("inf")
    
        if target_weight == 0:
            return 0
        elif target_weight > 0:
            for weight in egg_weights:
                sub_result = dp_make_weight(egg_weights, target_weight - weight)
                least_taken = min(least_taken, sub_result)
    
        return least_taken + 1
    
    if __name__ == "__main__":
        print(dp_make_weight((1, 6, 9), 14))
    

    对于每次调用,我们有 3 种可能性:

    1. 基本情况target_weight &lt; 0:返回一些内容以表明不可能有解决方案(为方便起见,我使用了无穷大)。
    2. 基本情况target_weight == 0:我们找到了一个候选解决方案。返回 0 表示此处未采取任何步骤,并为调用者提供一个要递增的基值。
    3. 递归案例target_weight &gt; 0:尝试通过从总数中减去每个可用的egg_weight 并递归探索根植于新状态的路径。在探索了当前状态的所有可能结果之后,选择用最少的步骤达到目标的结果。加 1 以计算当前步骤的取蛋并返回。

    到目前为止,我们已经看到贪婪的解决方案是不正确的以及如何解决它,但还没有激发动态编程或记忆。 DP和memoization是纯粹的优化概念,所以你可以在找到正确的解决方案并需要加速之后添加它们。上述解决方案的时间复杂度是指数级的:对于每个调用,我们都必须产生 len(egg_weights) 递归调用。

    有很多资源可以解释 DP 和 memoization,我相信你的课程涵盖了它,但简而言之,我们上面显示的递归解决方案通过采用不同的递归路径一遍又一遍地重新计算相同的结果,最终导致为target_weight 提供相同的值。如果我们在内存中保存一个备忘录(字典),将每次调用的结果存储在内存中,那么每当我们再次遇到调用时,我们就可以查找它的结果,而不是从头开始重新计算。

    def dp_make_weight(egg_weights, target_weight, memo={}):
        least_taken = float("inf")
    
        if target_weight == 0:
            return 0
        elif target_weight in memo:
            return memo[target_weight]
        elif target_weight > 0:
            for weight in egg_weights:
                sub_result = dp_make_weight(egg_weights, target_weight - weight)
                least_taken = min(least_taken, sub_result)
    
        memo[target_weight] = least_taken + 1
        return least_taken + 1
    
    if __name__ == "__main__":
        print(dp_make_weight((1, 6, 9, 12, 13, 15), 724)) # => 49
    

    由于我们使用的是 Python,“Pythonic”的做法可能是装饰函数。事实上,有一个内置的 memoizer 叫做 lru_cache,所以回到我们原来的函数没有任何 memoization,我们可以通过两行代码添加 memoization(缓存):

    from functools import lru_cache
    
    @lru_cache
    def dp_make_weight(egg_weights, target_weight):
        # ... same code as the top example ...
    

    使用装饰器进行记忆的缺点是调用堆栈的大小与包装器的大小成正比,因此它会增加爆栈的可能性。这是迭代编写 DP 算法的一个动机,自下而上(即,从解决方案基本案例开始,建立这些小解决方案的表格,直到您能够构建全局解决方案),这可能是一个很好的练习如果您正在寻找另一个角度,这个问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-03
      • 2021-02-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多