动态规划使用斐波那契数列引入了动态规划的概念

Posted 李威威的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划使用斐波那契数列引入了动态规划的概念相关的知识,希望对你有一定的参考价值。

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)

说明:

  1. 代码本身用于计算是没有问题的,但是仔细研究,我们就会发现,我们虽然使用递归实现了斐波那契数列在任意位置的值的计算,但是,如果要我们自己计算的话,肯定不会这样计算,因为太耗时了。

  2. 耗时的原因在于,在上述的递归实现中,存在大量的重复计算,例如:
    要计算 fib(4),就得计算 fib(3) 和 fib(2),
    要计算 fib(3),就得计算 fib(2) 和 fib(1),
    此时 fib(2) 就被重复计算了,下面是一张图,展示了部分重复计算的过程。

技术分享图片

  1. 要解决上一步的问题,就要避免重复计算,我们可以引入一个 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)针对“动态规划”问题的一般思考路径

我们通常的做法是:使用记忆化搜索的思路思考原问题,但是使用动态规划的方法来实现。即“从上到下”思考,但是“从下到上”实现。

技术分享图片

四、总结

对于一个递归结构的问题,如果我们在分析它的过程中,发现了它有很多“重叠子问题”,虽然并不影响结果的正确性,但是我们认为大量的重复计算是不环保,不简洁,不优雅,不高效的,因此,我们必须将“重叠子问题”进行优化,优化的方法就是“加入缓存”,“加入缓存”的一个学术上的叫法就是“记忆化搜索”。

另外,我们还发现,直接分析递归结构,是假设更小的子问题已经解决给出的实现,思考的路径是“自顶向下”。但有的时候,“自底向上”的思考路径往往更直接,这就是“动态规划”,我们是真正地解决了更小规模的问题,在处理更大规模的问题的时候,直接使用了更小规模问题的结果。

以上是关于动态规划使用斐波那契数列引入了动态规划的概念的主要内容,如果未能解决你的问题,请参考以下文章

动态规划之斐波那契数列

从最简单的斐波那契数列来学习动态规划。(JavaScript版本)

前端算法必知必会之动态规划-爬楼梯(斐波那契数列)

09动态规划解决斐波那契数列

动态规划-第一节2:动态规划之使用“斐波那契数列”问题说明重叠子问题如何解决

算法动态规划 - 斐波那契数