分治策略:
解决问题的典型策略:分而治之
将问题分为若干更小规模的部分
通过解决每一个小规模部分问题,并将结果汇总得到原问题的解
分治策略和递归算法的联系:
从找零问题中看贪心策略:
贪心策略(Greedy Method):每次都试图解决问题尽量大的一部分。
贪心策略解法:从最大面值的硬币开始,用尽量多的数量,有余额的,再到下一最大面值的硬币,还用尽量多的数量,一直到最小面值硬币为止。
递归解法:
兑换硬币最简单直接的情况就是需要兑换的面值正好等于某种硬币,就只用找零1枚硬币,也就是递归的基本结束条件。
其次需要减小问题的规模,我们要对每种硬币尝试1次,例如美元体系:找零减去1分(penny)后,求兑换硬币最少数量(递归调用自身);找零减去5分(nikel)后,求兑换硬币最少数量;找零减去10分(dime)后,求兑换硬币最少数量;找零减去25分(quarter)后,求兑换硬币最少数量;上述4项中选择最小的一个。
# 效率极差递归解法:
def recMC(coinValueList,change):
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
print(recMC([1,5,10,25],63))
上述代码效率低下的重要原因就是重复计算太多,算法改进的关键就在于消除重复计算。可以用一个表将计算过的中间结果保存起来,在计算之前查表看看是否已经计算过。这个算法的中间结果就是部分找零的最优解,在递归调用过程中已经得到的最优解被记录下来。在递归调用之前,先查找表中是否已有部分找零的最优解,如果有,直接返回最优解而不进行递归调用,如果没有才进行递归调用。
这种技术叫做memoization(记忆化/函数值缓存)技术。
# 递归解法改进代码:
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 < minCoins:
minCoins = numCoins
# 找到最优解,记录到表中
knownResults[change] = minCoins
return minCoins
print(recDC([1,5,10,25],63,[0]*64))
def dpMakeChange(coinValueList,change,minCoins):
# 从1分开始到change逐个计算最少硬币数
for cents in range(1,change+1):
# 1. 初始化一个最大值
coinCount = cents
# 2. 减去每个硬币,向后查最少硬币数,同时记录总的最少数
for j in [c for c in coinValueList if c <= cents]:
if minCoins[cents - j] + 1 < coinCount:
coinCount = minCoins[cents - j] + 1
# 得到当前最少硬币数,记录在表中
minCoins[cents] = coinCount
# 返回最后一个结果
return minCoins[change]
print(dpMakeChange([1,5,10,21,25],63,[0]*64))
我们注意到动态规划算法的dpMakeChange并不是递归函数,虽然这个问题是从递归算法开始解决,但最终我们得到一个更有条理的高效非递归算法。
动态规划中最主要的思想是: 从最简单的情况开始到达所需找零的循环,其每一步都依靠以前的最优解来得到本步骤的最优解,直到得到答案。
代码改进:返回硬币如何组合
# 代码改进:返回硬币如何组合
def dpMakeChange(coinValueList,change,minCoins,coinsUsed):
for cents in range(1,change+1):
coinCount = cents
newCoin = 1 # 初始化一下新加硬币
for j in [c for c in coinValueList if c <= cents]:
if minCoins[cents - j] + 1 < coinCount:
coinCount = minCoins[cents - j] + 1
newCoin = j # 对应最小数量,所减的硬币
minCoins[cents] = coinCount
coinsUsed[cents] = newCoin
return minCoins[change]
def printCoins(coinsUsed,change):
coin = change
while coin > 0:
thisCoin = coinsUsed[coin]
print(thisCoin)
coin = coin - thisCoin
amnt = 63
clist = [1,5,10,21,25]
coinsUsed = [0]*(amnt+1)
coinCount = [0]*(amnt+1)
print("Making change for",amnt,"requires")
print(dpMakeChange(clist,amnt,coinCount,coinsUsed),"coins")
print("They are:")
printCoins(coinsUsed,amnt)
print("The used list is as follows:")
print(coinsUsed)
# 宝物的重量和价值
tr = [None,{'w':2,'v':3},{'w':3,'v':4},{'w':4,'v':8},
{'w':5,'v':8},{'w':9,'v':10}]
max_w = 20 # 大盗最大承重
# 初始化二维表格m[(i,w)],表示前i个宝物中,最大重量w的组合,所得到的最大价值
# 当i什么都不取,或w上限为0,价值均为0
m = {(i,w):0 for i in range(len(tr))
for w in range(max_w+1)}
# 逐个填写二维表格
for i in range(1,len(tr)):
for w in range(1,max_w+1):
if tr[i]['w'] > w: # 装不下第i个宝物
m[(i,w)] = m[(i-1,w)] # 不装第i个宝物
else:
# 不装第i个宝物,装第i个宝物,两种情况下最大价值
m[(i,w)] = max(
m[(i-1,w)],
m[(i-1,w-tr[i]['w'])] + tr[i]['v'])
print(m[(len(tr)-1,max_w)])
递归:
# 宝物的重量和价值
tr = {(2,3),(3,4),(4,8),(5,8),(9,10)}
max_w = 20 # 大盗最大承重
m = {} # 初始化记忆化表格m,key是(宝物组合,最大重量),value是最大价值
def thief(tr,w):
if tr == set() or w == 0:
m[(tuple(tr),w)] = 0
return 0
elif (tuple(tr),w) in m: # 若该组合已经存在与记忆化表格m,就直接返回
return m[(tuple(tr),w)]
else:
vmax = 0
for t in tr:
if t[0] <= w:
# 逐个从集合中去掉某个宝物,递归调用
# 选出所有价值中的最大值
v = thief(tr-{t},w-t[0]+t[1])
vmax = max(vmax,v)
m[(tuple(tr),w)] = vmax
return vmax
print(thief(tr,max_w))