1. 递归
1.1 递归三要素
递归 (recursion) 通过自身调用来降低问题的复杂性,利用递归解决问题首先需要确定 3 个子问题:
- 基本问题:最简单的子问题,或称为初始状态
- 状态转移方程:如何从复杂问题向更简单的问题过渡
- 目标问题:需要利用状态转移方程从基本问题到复杂问题
1.2 最小钱币数问题
最小钱币数是一个找零问题,比如目前有若干 [1, 5, 10] 三种硬币,需要找零 37,则在使用最少钱币的情况下需要 10, 10, 10, 5, 1, 1 共 6 枚硬币
使用递归解决该问题:
- 基本问题:当找零数为某种硬币金额时,返回 1
- 状态转移方程:尝试先给出 1 枚任意金额小于找零金额的硬币,再找零剩余部分,从中选择硬币数目最少的
- 目标问题:找到需要的最少硬币数
def recMinCoins(coinValueList, change):
res = change
if change in coinValueList:
return 1
else:
for coin in [c for c in coinValueList if c < change]:
num = minCoins(coinValueList, change - coin) + 1
if num < res:
res = num
return res
s = time.time()
print(recMinCoins([1, 2, 5, 10], 37)) # 5
e = time.time()
print(e - s)
分析:结果正确,但消耗的时间和内存巨大(耗时 26.66s),因为在递归调用自身时,会重复计算很多参数完全相同的函数,比如 37 先给出 10 以及 37 先给出 2 枚 5 的硬币后,接下来的子问题完全一样,即找零 27,但该程序仍然会重复计算该子问题!
2. 动态规划
2.1 方法
动态规划 (dynamic programming) 是优化后的递归算法,它通过存储先前计算过的子问题结果,来避免相同子问题的重复计算,从而提高算法的效率。
算法步骤:
-
创建表格,假设要计算的问题复杂度为 n,则创建 n 列的表格
minCoins
,minCoins[0] = 0,硬币为coinValue
= [1, 2, 5]
-
接着按列从 0 到 n 依次计算结果 minCoins[i],对于 minCoins[i],尝试使用每一种硬币 conValue[j],j = 0, 1, 2;若
minCoins[i] < coinValue[j]
,则目标硬币数为minCoins[i - coinValue(j)] + 1
,计算全部可能的情况取最小值
-
比如说下面这种情况,要计算 minCoins[16],有四种情况:
minCoins[13] + 1 = 5
minCoins[12] + 1 = 4
minCoins[9] + 1 = 4
最后 minCoin[14] = 4(选择 5 5 2 2 共 4 枚硬币)
2.2 动态规划实现最小钱币数
import time
import numpy as np
def dpMinCoins(coinValueList, change):
minCoins = [0]*(change + 1)
for money in range(1, change+1):
options = []
for val in coinValueList:
if money >= val:
options.append(minCoins[money - val] + 1)
minCoins[money] = np.min(options)
return minCoins[change]
s = time.time()
print(dpMinCoins([1, 2, 5, 10], 37)) # 5
e = time.time()
print(e - s)
运行结果:5,运行时间接近 0s 🍻
此时创建的数组如下:
[0, 1, 1, 2, 2, 1, 2, 2, 3, 3, 1, 2, 2, 3, 3, 2, 3, 3, 4, 4, 2, 3, 3, 4, 4, 3, 4, 4, 5, 5, 3, 4, 4, 5, 5, 4, 5, 5]
此外,利用 minCoins[money] == minCoins[money - val] + 1
可以倒推使用过的硬币:
import time
import numpy as np
def dpMinCoins(coinValueList, change):
minCoins = [0]*(change + 1)
coinUsed = [[0]*(change + 1) for i in range(len(coinValueList))]
for money in range(1, change+1):
# calculate coins
options = []
for val in coinValueList:
if money >= val:
options.append(minCoins[money - val] + 1)
minCoins[money] = np.min(options)
# coins record
for val in coinValueList:
if money >= val and minCoins[money] == minCoins[money - val] + 1:
for i in range(len(coinValueList)):
coinUsed[i][money] = coinUsed[i][money - val]
coinUsed[coinValueList.index(val)][money] += 1
break
print(minCoins)
for row in coinUsed:
print(row)
return minCoins[change]
s = time.time()
print(dpMinCoins([1, 2, 5, 10], 37)) # 5
e = time.time()
print(e - s)
结果为:
[0, 1, 1, 2, 2, 1, 2, 2, 3, 3, 1, 2, 2, 3, 3, 2, 3, 3, 4, 4, 2, 3, 3, 4, 4, 3, 4, 4, 5, 5, 3, 4, 4, 5, 5, 4, 5, 5]
[0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0]
[0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1, 1, 2, 0, 0, 1]
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3]
5
0.0009975433349609375
总结:
- 所有递归算法都必须回归到基本问题。
- 递归算法必须改变其状态并向基本问题靠近。
- 递归在某些情况下可以替代循环。
- 递归有时比其他算法的计算复杂度更高。
完结 🍻