您的位置:首页 > 理论基础 > 数据结构算法

基于python的数据结构和算法(北京大学)第六章(贪心策略和动态规划)

2020-07-24 17:24 288 查看
  • 分治策略:
    解决问题的典型策略:分而治之
    将问题分为若干更小规模的部分
    通过解决每一个小规模部分问题,并将结果汇总得到原问题的解

    分治策略和递归算法的联系:

  • 从找零问题中看贪心策略:
    贪心策略(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))
  • 动态规划法:
    从最简单的“1分钱找零”的最优解开始,逐步递加上去,直到我们需要的找零钱数。
    在找零递加的过程中,设法保持每一分钱的递加都是最优解,一直加到求解找零钱数,自然得到最优解。
    采用动态规划来解决11分钱的兑换问题:
    从1分钱兑换开始,逐步建立一个兑换表。

    计算11分钱的兑换法,我们做如下几步:
    1.首先减去1分硬币,剩下10分钱查表最优解是1(2个)
    2.然后减去5分硬币,剩下6分钱查表最优解是2(3个)
    3.最后减去10分硬币,剩下1分钱查表最优解是1(2个)

通过上述最小值得到最优解:2个硬币

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)
  • 博物馆大盗问题:
    大盗潜入博物馆,面前有5件宝物,分别有重量和价值,大盗的背包仅能负重20公斤,请问如何选择宝物,总价值最高?

    我们把m(i, W)记为:前i(1 <= i <= 5)个宝物中,组合不超过W(1 <= W <= 20)重量,得到的最大价值m(i, W)应该是m(i-1, W)和m(i-1, W-Wi)+vi两者最大值。我们从m(1, 1)开始计算到m(5, 20)。


    动态规划:
# 宝物的重量和价值
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))
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: