9-1 使用斐波那契数列引入了动态规划的概念
一、计算斐波那契数列的第 $n$ 项数值
1、斐波那契数列的定义
斐波那契数列是通过"递归"定义的,通过这个递归关系式,我们可以知道斐波那契数列中任意一个位置的数值。
$$
\begin{equation}\begin{split}
F(0) & = 0,\
F(1) & = 1,\
F(n) & = F(n-1) + F(n-2),\
\end{split}\end{equation}
$$
2、第 1 版 Python 代码实现:使用斐波那契数列的定义式子递归实现
很容易地,我们能写出下面的代码:
def fib(n):
if n == 0:
return 0
if n == 1:
return 1
return fib(n - 1) + fib(n - 2)
说明:
代码本身用于计算是没有问题的,但是仔细研究,我们就会发现,我们虽然使用递归实现了斐波那契数列在任意位置的值的计算,但是,如果要我们自己计算的话,肯定不会这样计算,因为太耗时了。
耗时的原因在于,在上述的递归实现中,存在大量的重复计算,例如:
要计算 fib(4),就得计算 fib(3) 和 fib(2),
要计算 fib(3),就得计算 fib(2) 和 fib(1),
此时 fib(2) 就被重复计算了,下面是一张图,展示了部分重复计算的过程。
- 要解决上一步的问题,就要避免重复计算,我们可以引入一个 memo 数组,用于存入已经计算过一次的 fib 的值,下一次需要这个值的时候,再从中取,下面是代码实现。
3、第 2 版 Python 代码实现:加入了记忆化搜索,即使用了缓存数组,以避免重复计算
memo = None
def _fib(n):
if memo[n] != -1:
return memo[n]
if n == 0:
return 0
if n == 1:
return 1
memo[n] = _fib(n - 1) + _fib(n - 2)
return memo[n]
def fib(n):
global memo
memo = [-1] * (n + 1)
return _fib(n)
4、第 3 版 Python 代码实现:虽然很简单,但是我们就可以称之为“动态规划”的解法
这个版本是最接近我们自己去计算斐波那契数列的第 $n$ 项。想一想的确是这样,聪明的你一定不会递归去计算波那契数列的,因为我们的脑容量是有限,不太适合做太深的递归思考,虽然计算机在行递归,但是我们也没有必要让计算机做重复的递归工作。
def fib(n):
memo = [-1] * (n + 1)
memo[0] = 0
memo[1] = 1
for i in range(2, n + 1):
memo[i] = memo[i - 1] + memo[i - 2]
return memo[n]
二、什么是“记忆化搜索”
针对一个递归问题,如果它呈树形结构,并且出现了很多”重叠子问题”,会导致计算效率低下,“记忆化搜索”就是针对”重叠子问题”的一个解决方案,实际上就是”加缓存避免重复计算”。
三、什么是“动态规划”
(1)比较“记忆化搜索”与“动态规划”
由上面的介绍我们就可以引出动态规划的概念:
- "记忆化搜索"或者我们称"重叠子问题"的加缓存优化的实现,我们的思考路径是"自顶向下"。即为了解决数据规模大的问题,我们“假设”已经解决了数据规模较小的子问题。
- “动态规划”就是上述"循环版本"的实现,我们思考问题路径是"自下而上"。实际上,我们是先“真正地”解决了数据规模较小的问题,然后一步一步地解决了数据规模较大的问题。
(2)“动态规划”的官方定义
下面我们给出“动态规划”的官方定义:
dynamic programming (also known as dynamic optimization) is a method for solving a complex problem by breaking it down into a collection of simpler subproblems, solving each of those subproblems just once, and storing their solutions – ideally, using a memory-based data structure.
将原问题拆解成若干子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。
(3)针对“动态规划”问题的一般思考路径
我们通常的做法是:使用记忆化搜索的思路思考原问题,但是使用动态规划的方法来实现。即“从上到下”思考,但是“从下到上”实现。
四、总结
对于一个递归结构的问题,如果我们在分析它的过程中,发现了它有很多“重叠子问题”,虽然并不影响结果的正确性,但是我们认为大量的重复计算是不环保,不简洁,不优雅,不高效的,因此,我们必须将“重叠子问题”进行优化,优化的方法就是“加入缓存”,“加入缓存”的一个学术上的叫法就是“记忆化搜索”。
另外,我们还发现,直接分析递归结构,是假设更小的子问题已经解决给出的实现,思考的路径是“自顶向下”。但有的时候,“自底向上”的思考路径往往更直接,这就是“动态规划”,我们是真正地解决了更小规模的问题,在处理更大规模的问题的时候,直接使用了更小规模问题的结果。