上一小节.
1、分治策略
也就是说,递归就是分治策略的具体应用。
2、动态规划
在计算机科学中,许多程序是为使一些问题得到最优解而写;例如,找到两点间的最短路径,找到最匹配一组点的线,或找到满足某些条件的最小对象集。计算机学家有许多策略来解决这些问题。动态规划是这类求最优解问题的解决策略之一。
优化问题的一个典型例子就是用最少的硬币来找零。问题是这样的:
假设你是一家自动售货机制造商的程序员。你的公司正设法在每一笔交易找零时都能提供最少数目的硬币以便工作能更加简单。假设一个顾客投了1美元来购买37美分的物品。你用来找零的硬币的最小数量是多少?答案是六枚硬币:两个25 美分,一个10美分,三个1美分。我们是怎么得到六个硬币这个答案的呢?首先我们要使用面值中最大的硬币(25美分),并且尽可能多的使用它,接着我们再使用下一个可使用的最大面值的硬币,也是尽可能多的使用。这种方法被称为贪心算法,因为我们试图尽可能快的解决一个问题。
贪心算法很容易,既容易想又容易用代码实现。核心思想是:每次选择当前最优的解。
但是有的问题不能用贪心算法解决,如下面:
接下来看看无论硬币的面值怎么变化,都能找到问题最优解的算法。
2.1 动态规划实战:找零兑换问题
(1)找零兑换问题的递归解法:
递归解法的思想:
实现代码:
def recMC(coinValueList,change):
"""
:param coinValueList: 为硬币的面额
:param change:要找零的钱
:return:找零硬币的最小数量
"""
minCoins = change
if change in coinValueList: # 如果找零的钱在硬币的面额里,这个就是边界
return 1
else:
for i in [c for c in coinValueList if c <= change]: # 这个就是对每个面额进行尝试,递归调用
numCoins = 1 + recMC(coinValueList,change-i)
if numCoins < minCoins:
minCoins = numCoins
return minCoins
if __name__ == "__main__":
print(recMC([1,5,10,25],63))
代码解释:
在第8行,我们检查基本结束条件;也就是说,需要兑换的找零数等于我们硬币的某个面值。如果我们没有等于找零数目的硬币面额,那么我们就对每个小于我们找零总数的不同的硬币值调用递归。第11行展示了我们应该怎样通过使用一个硬币面值的列表,以帮助我们筛选出比当前找零价值小的硬币的列表。通过选定硬币的值,递归调用减小了我们需要找零的零钱总数。第12行展示了递归调用。注意,在同一行,我们要给硬币总数加1,这是因为我们使用了一枚硬币。只需加1就相当于:满足基本结束条件时,我们就做一次递归调用。
递归解法代码很简单,但是最大的问题是重复计算太多,导致运算效率很低。例如,下图显示26分钱的找零,图中每一个节点对应一次recMC函数调用。节点上的数字显示了我们要计算的需要找零的硬币总量。箭头上的数字则显示了我们使用的硬币的面额。顺着图形,我们可以看到图中任何点的硬币的组合。这种算法会重复计算为15美分找零的最优解至少三次。每一次这种计算都要调用52次函数。显然我们浪费了大量的时间和精力来重复计算旧的结果。
所以能改进上述的递归解法:
减少工作量的关键在于记住一些出现过的结果,这样就能避免重复计算我们已经知道的结果。一个简单的解决方案就是我们将所找到硬币找零的最小数目存储在一个表中。然后在我们计算一个新的最小值之前,可以先查表看这个结果是否已知。如果表中已经有了这个结果,我们就可以从表中引用这个值而不是重复计算。
下面是改善的代码:
def recDC(coinValueList,change,knownResults):
minCoins = change
if change in coinValueList: # 递归基本结束条件
knownResults[change] = 1 # 记录最优解
return 1
elif knownResults[change] > 0:
return knownResults[change] # 查表成功,直接使用最优解
else:
for i in [c for c in coinValueList if c <= change]:
numCoins = 1 + recDC(coinValueList, change-i, knownResults)
if numCoins