您要解决的是硬币找零问题的一个变体。在这里,您正在寻找最小数量的零钱,或总和为给定数量的最小硬币数量。
考虑一个简单的情况,您的数组是
c = [1, 2, 3]
您将 5 写为 C 中元素的组合,并想知道这样的最短组合是什么。这里 C 是硬币值的集合,5 是你想要找零的数量。
让我们写下所有可能的组合:
1 + 1 + 1 + 1 + 1
1 + 1 + 1 + 2
1 + 2 + 2
1 + 1 + 3
2 + 3
请注意,两个组合在重新排序之前是相同的,例如 2 + 3 = 3 + 2。
这里有一个很棒的结果,乍一看并不明显,但很容易证明。如果您有任何硬币/价值序列,它是一个总和为给定数量的最小长度序列,无论您如何拆分此序列,这两个部分也将是相应数量的最小长度序列。
例如,如果c[3] + c[1] + c[2] + c[7] + c[2] + c[3] 加起来等于S,并且我们知道6 是来自c 的任何元素序列的最小长度加起来S 那么如果你拆分
|
S = c[3] + c[1] + c[2] + c[7] | + c[2] + c[3]
|
您知道4 是加起来为c[3] + c[1] + c[2] + c[7] 的序列的最小长度,2 是加起来为c[2] + c[3] 的序列的最小长度。
|
S = c[3] + c[1] + c[2] + c[7] | + c[2] + c[3]
|
= S_left + S_right
如何证明这一点?相反,假设S_left 的长度不是最优的,即有一个较短的序列加起来等于S_left。但是我们可以将S 写成这个较短序列和S_right 的总和,从而与S 的长度最小的事实相矛盾。 □
由于无论您如何拆分序列都是如此,您可以使用此结果来构建遵循动态编程范式原则的递归算法(解决较小的问题,同时可能跳过不会使用的计算,memoization 或跟踪计算值,最后结合结果)。
由于这种维持子问题最优性的特性,硬币问题也被称为“表现出最优子结构”。
好的,所以在上面的小例子中,我们将如何使用动态规划方法来解决问题:假设我们想从c = [1, 2, 3] 中找到最短的元素序列来写入和5。我们求解减去一枚硬币得到的子问题:5 - 1、5 - 2和5 - 3,我们取这些子问题的最小解并加1(丢失的硬币)。
所以我们可以写一些类似的东西
shortest_seq_length([1, 2, 3], 5) =
min( shortest_seq_length([1, 2, 3], 5-1),
shortest_seq_length([1, 2, 3], 5-2),
shortest_seq_length([1, 2, 3], 5-3)
) + 1
自下而上编写算法很方便,从可以保存并用于形成更大和的和的较小值开始。我们只是解决从 1 到所需总和的所有可能值的问题。
这是 Python 中的代码:
def shortest_seq_length(c, S):
res = {0: 0} # res contains computed results res[i] = shortest_seq_length(c, i)
for i in range(1, S+1):
res[i] = min([res[i-x] for x in c if x<=i]) + 1
return res[S]
现在,除了我们无法为i 的所有值填充记忆结构的情况外,这可以正常工作。当我们在c 中没有值1 时就是这种情况,因此例如,如果c = [2, 5] 和上面的函数,我们就不能形成总和1
shortest_seq_length([2, 3], 5)
# ValueError: min() arg is an empty sequence
所以要解决这个问题,例如可以使用 try/catch:
def shortest_seq_length(c, S):
res = {0: 0} # res is the dictionary containing results for each sum res[i] = shortest_seq_length(c, i)
for i in range(1, S+1):
try:
res[i] = min([res[i-x] for x in c if x<=i and res[i-x] is not None]) +1
except:
res[i] = None # takes care of error when [res[i-x] for x in c if x<=i] is empty
return res[S]
试试看:
print(shortest_seq_length([2, 3], 5))
# 2
print(shortest_seq_length([1, 5, 10, 25], 37))
# 4
print(shortest_seq_length([1, 5, 10], 30))
# 3
print(shortest_seq_length([1, 5, 10], 25))
# 3
print(shortest_seq_length([1, 5, 10], 29))
# 7
print(shortest_seq_length([5, 10], 9))
# None
算法的一个小改进是当总和等于其中一个值/硬币时跳过计算最小值的步骤,但是如果我们编写一个循环来计算最小值,这可以做得更好。然而,这种改进并没有提高O(mS) 的整体复杂性,而m = len(c)。