动态规划之记忆的作用
动态规划(Dynamic&Programming)算法由20世纪50年代初美国数学家RE.Bellman创立,一般用于求解优化问题,问题中常见的词有最大、最小、最重、最长等等。
动态规划是一种高效地穷举算法,通过特定的数据结构设计达到高效的效果。为了方便理解,可以把动态规划分为三个关键字:
递归+记忆+猜测
其中猜测用来构造递归,记忆用来实现递归
一.动态规划算法求解步骤
1. 定义子问题。子问题与原问题的区别在于输入数据的规模不同。
2. 建立子问题之间的关系。一般是递归关系。
(1) 猜测(类似于递归和分治算法中的不妨设),启动求解过程;
(2) 子问题的解—>节点,子问题求解过程—>节点遍历,而节点的遍历需要满足拓扑排序。所谓拓扑排序简单来说就是:当前节点只与已求得值的节点有关;
3. 明确边界条件。
4. 得到原问题的解。一般在表上遍历得到(自底向上)。
5. 分析算法时间复杂度。T(n)=子问题个数x每一个子问题的时间
二.记忆的作用
我们通过再遇斐波那契数来说明动态规划中记忆的作用
斐波那契数:0 1 1 2 3 5 8...从第2项开始,后一个数的值等于前两个数之和
Input:输入一个整数n,
Output:输出斐波那契数列的第n项
递推方程如下所示:
算法1:常用的递归算法实现
# 一般的递归
def fib_rec(n):
if n <= 1: # 边界条件
f = n
else:
f = fib_rec(n - 1) + fib_rec(n - 2) # 递归调用
return f
采用递归的时间复杂度为O(2^n),空间复杂度为O(n),虽然递归方法代码简洁,容易理解,但是效率很低,且有大量的重复计算。
算法2:动态规划
利用记忆提高实现效率
如上图所示,在展开递归树时,将已经求出的值存储在一张额外的表中(此处的表即为特定的数据结构),下一次展开计算时,先查看表中是否已经有记录,如果有,则直接取用,不用展开,如果没有,则重复上一步。
实现1:自上而下
# 动态规划:自上而下的实现
memo = {} # 存储结果的字典
def fib_top_down(n):
if n in memo: # 查表
return memo[n]
else:
if n <= 1:
f = n
else:
f = fib_top_down(n - 1) + fib_top_down(n - 2) # 递归调用
memo[n] = f # 自上而下填表
return f
由于没有重复计算,每个节点只展开一次,所以算法时间复杂度T(n)=O(n)
实现2:自底向上
此外,还有一种更高效的动态规划实现方法,即自底向上,不用递归,而是用循环实现
# 动态规划:自底向上的实现
def fib_bottom_up(n):
fib = {} # 存储结果的字典
for k in range(n + 1):
if k <= 1: # 边界条件
f = n
else:
f = fib[k - 1] + fib[k - 2] # 自底向上填表
fib[k] = f
return fib[n]
时间复杂度同样为O(n),但是没有用到递归调用(注意:代码中fib是字典而不是函数)。自底向上的实现方法更为常用。
END
往期回顾