1 回答
TA贡献1998条经验 获得超6个赞
这里的问题是典型的 DP 情况,贪婪有时可以给出最优解,但有时不能。
这个问题的情况类似于经典的 DP 问题硬币找零,在给定目标值的情况下,我们希望找到最少数量的不同价值硬币来找零。在美国等一些国家(使用价值 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 种可能性:
基本情况
target_weight < 0
:返回一些东西以表明不可能有解决方案(为了方便起见,我使用了无穷大)。基本案例
target_weight == 0
:我们找到了一个候选解决方案。返回 0 表示此处未采取任何步骤,并为调用者提供一个要递增的基值。递归案例
target_weight > 0
:尝试egg_weight
通过从总数中减去每个可用的值并递归地探索根植于新状态的路径。在探索了当前状态的所有可能结果之后,选择用最少的步骤达到目标的结果。加 1 计数当前步骤的取蛋并返回。
到目前为止,我们已经看到贪婪的解决方案是不正确的以及如何修复它,但还没有激发动态编程或记忆。DP和memoization是纯粹的优化概念,所以你可以在找到正确的解决方案并需要加速之后添加它们。上述解决方案的时间复杂度是指数级的:对于每个调用,我们都必须产生len(egg_weights)
递归调用。
有很多资源可以解释 DP 和记忆化,我相信你的课程涵盖了它,但简而言之,我们上面显示的递归解决方案通过采用不同的递归路径一遍又一遍地重新计算相同的结果,最终导致给出相同的值为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 算法的一个动机,自下而上(即,从解决方案基本案例开始,建立这些小解决方案的表格,直到您能够构建全局解决方案),这可能是一个很好的练习如果您正在寻找另一个角度,这个问题。
添加回答
举报