您要解决的是硬币找零问题的变体。在这里,您正在寻找最小数量的零钱,或总和为给定数量的最小硬币数量。
考虑一个简单的情况,您的数组是
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
□</p>
由于无论您如何拆分序列都是如此,您可以使用此结果来构建遵循动态编程范式原则的递归算法(解决较小的问题,同时可能跳过不会使用的计算、记忆或跟踪计算值,最后结合结果)。
由于这种维持子问题最优性的特性,硬币问题也被称为“表现出最优子结构”。
好的,所以在上面的小例子中,我们将如何使用动态编程方法解决问题:假设我们想要找到最短的元素序列c = [1, 2, 3]
来编写 sum 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
. 当我们没有 in 的值时就是这种情况1
,c
例如,1
如果c = [2, 5]
我们得到上述函数,我们就无法形成总和
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 contains 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]
或者没有尝试/捕获:
def shortest_seq_length(c, S):
res = {0: 0} # res[i] = shortest_seq_length(c, i)
for i in range(1, S+1):
prev = [res[i-x] for x in c if x<=i and res[i-x] is not None]
if len(prev)>0:
res[i] = min(prev) +1
else:
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
不仅要显示长度,还要显示最小长度硬币的组合:
from collections import defaultdict
def shortest_seq_length(coins, sum):
combos = defaultdict(list)
combos[0] = [[]]
for i in range(1, sum+1):
for x in coins:
if x<=i and combos[i-x] is not None:
for p in combos[i-x]:
comb = sorted(p + [x])
if comb not in combos[i]:
combos[i].append(comb)
if len(combos[i])>0:
m = (min(map(len,combos[i])))
combos[i] = [combo for i, combo in enumerate(combos[i]) if len(combo) == m]
else:
combos[i] = None
return combos[sum]
total = 9
coin_sizes = [10, 8, 5, 4, 1]
shortest_seq_length(coin_sizes, total)
# [[1, 8], [4, 5]]
要显示所有序列,请删除最小计算:
from collections import defaultdict
def all_seq_length(coins, sum):
combos = defaultdict(list)
combos[0] = [[]]
for i in range(1, sum+1):
for x in coins:
if x<=i and combos[i-x] is not None:
for p in combos[i-x]:
comb = sorted(p + [x])
if comb not in combos[i]:
combos[i].append(comb)
if len(combos[i])==0:
combos[i] = None
return combos[sum]
total = 9
coin_sizes = [10, 5, 4, 8, 1]
all_seq_length(coin_sizes, total)
# [[4, 5],
# [1, 1, 1, 1, 5],
# [1, 4, 4],
# [1, 1, 1, 1, 1, 4],
# [1, 8],
# [1, 1, 1, 1, 1, 1, 1, 1, 1]]
该算法的一个小改进是当总和等于其中一个值/硬币时跳过计算最小值的步骤,但是如果我们编写一个循环来计算最小值,这可以做得更好。O(mS)
然而,这并没有提高m = len(c)
.